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
24 log = logging.getLogger('opds')
26 from stats.utils import piwik_track
31 u"link": u"opds_user",
33 u"title": u"Moje półki",
34 u"description": u"Półki użytkownika dostępne po zalogowaniu"
37 u"category": u"author",
38 u"link": u"opds_by_category",
39 u"link_args": [u"author"],
41 u"description": u"Utwory wg autorów"
45 u"link": u"opds_by_category",
46 u"link_args": [u"kind"],
48 u"description": u"Utwory wg rodzajów"
51 u"category": u"genre",
52 u"link": u"opds_by_category",
53 u"link_args": [u"genre"],
55 u"description": u"Utwory wg gatunków"
58 u"category": u"epoch",
59 u"link": u"opds_by_category",
60 u"link_args": [u"epoch"],
62 u"description": u"Utwory wg epok"
68 return urljoin("http://%s" % Site.objects.get_current().domain, url)
71 class OPDSFeed(Atom1Feed):
72 link_rel = u"subsection"
73 link_type = u"application/atom+xml"
75 _book_parent_img = full_url(os.path.join(settings.STATIC_URL, "img/book-parent.png"))
77 _book_parent_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
79 _book_parent_img_size = ''
81 _book_img = full_url(os.path.join(settings.STATIC_URL, "img/book.png"))
83 _book_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
88 def add_root_elements(self, handler):
89 super(OPDSFeed, self).add_root_elements(handler)
90 handler.addQuickElement(u"link", None,
91 {u"href": reverse("opds_authors"),
93 u"type": u"application/atom+xml"})
94 handler.addQuickElement(u"link", None,
95 {u"href": full_url(os.path.join(settings.STATIC_URL, "opensearch.xml")),
97 u"type": u"application/opensearchdescription+xml"})
100 def add_item_elements(self, handler, item):
101 """ modified from Atom1Feed.add_item_elements """
102 handler.addQuickElement(u"title", item['title'])
104 # add a OPDS Navigation link if there's no enclosure
105 if item['enclosure'] is None:
106 handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"subsection", u"type": u"application/atom+xml"})
107 # add a "green book" icon
108 handler.addQuickElement(u"link", '',
109 {u"rel": u"http://opds-spec.org/thumbnail",
110 u"href": self._book_parent_img,
111 u"length": self._book_parent_img_size,
112 u"type": u"image/png"})
113 if item['pubdate'] is not None:
114 # FIXME: rfc3339_date is undefined, is this ever run?
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 # FIXME: get_tag_uri is undefined, is this ever run?
132 unique_id = get_tag_uri(item['link'], item['pubdate'])
133 handler.addQuickElement(u"id", unique_id)
136 # OPDS needs type=text
137 if item['description'] is not None:
138 handler.addQuickElement(u"summary", item['description'], {u"type": u"text"})
140 # Enclosure as OPDS Acquisition Link
141 if item['enclosure'] is not None:
142 handler.addQuickElement(u"link", '',
143 {u"rel": u"http://opds-spec.org/acquisition",
144 u"href": item['enclosure'].url,
145 u"length": item['enclosure'].length,
146 u"type": item['enclosure'].mime_type})
147 # add a "red book" icon
148 handler.addQuickElement(u"link", '',
149 {u"rel": u"http://opds-spec.org/thumbnail",
150 u"href": self._book_img,
151 u"length": self._book_img_size,
152 u"type": u"image/png"})
155 for cat in item['categories']:
156 handler.addQuickElement(u"category", u"", {u"term": cat})
159 if item['item_copyright'] is not None:
160 handler.addQuickElement(u"rights", item['item_copyright'])
163 class AcquisitionFeed(Feed):
165 link = u'http://www.wolnelektury.pl/'
166 item_enclosure_mime_type = "application/epub+zip"
167 author_name = u"Wolne Lektury"
168 author_link = u"http://www.wolnelektury.pl/"
170 def item_title(self, book):
173 def item_description(self):
176 def item_link(self, book):
177 return book.get_absolute_url()
179 def item_author_name(self, book):
181 return book.tags.filter(category='author')[0].name
185 def item_author_link(self, book):
187 return book.tags.filter(category='author')[0].get_absolute_url()
191 def item_enclosure_url(self, book):
192 return full_url(book.epub_file.url) if book.epub_file else None
194 def item_enclosure_length(self, book):
195 return book.epub_file.size if book.epub_file else None
198 class RootFeed(Feed):
200 title = u'Wolne Lektury'
201 link = u'http://wolnelektury.pl/'
202 description = u"Spis utworów na stronie http://WolneLektury.pl"
203 author_name = u"Wolne Lektury"
204 author_link = u"http://wolnelektury.pl/"
209 def item_title(self, item):
212 def item_link(self, item):
213 return reverse(item['link'], args=item['link_args'])
215 def item_description(self, item):
216 return item['description']
219 class ByCategoryFeed(Feed):
221 link = u'http://wolnelektury.pl/'
222 description = u"Spis utworów na stronie http://WolneLektury.pl"
223 author_name = u"Wolne Lektury"
224 author_link = u"http://wolnelektury.pl/"
226 def get_object(self, request, category):
227 feed = [feed for feed in _root_feeds if feed['category'] == category]
235 def title(self, feed):
238 def items(self, feed):
239 return Tag.objects.filter(category=feed['category']).exclude(book_count=0)
241 def item_title(self, item):
244 def item_link(self, item):
245 return reverse("opds_by_tag", args=[item.category, item.slug])
247 def item_description(self):
251 class ByTagFeed(AcquisitionFeed):
253 return tag.get_absolute_url()
255 def title(self, tag):
258 def description(self, tag):
259 return u"Spis utworów na stronie http://WolneLektury.pl"
261 def get_object(self, request, category, slug):
262 return get_object_or_404(Tag, category=category, slug=slug)
264 def items(self, tag):
265 books = Book.tagged.with_any([tag])
266 l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books.iterator()])
267 descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
269 books = books.exclude(pk__in=descendants_keys)
274 @factory_decorator(logged_in_or_basicauth())
276 class UserFeed(Feed):
278 link = u'http://www.wolnelektury.pl/'
279 description = u"Półki użytkownika na stronie http://WolneLektury.pl"
280 author_name = u"Wolne Lektury"
281 author_link = u"http://wolnelektury.pl/"
283 def get_object(self, request):
286 def title(self, user):
287 return u"Półki użytkownika %s" % user.username
289 def items(self, user):
290 return Tag.objects.filter(category='set', user=user).exclude(book_count=0)
292 def item_title(self, item):
295 def item_link(self, item):
296 return reverse("opds_user_set", args=[item.slug])
298 def item_description(self):
301 # no class decorators in python 2.5
302 #UserFeed = factory_decorator(logged_in_or_basicauth())(UserFeed)
305 @factory_decorator(logged_in_or_basicauth())
307 class UserSetFeed(AcquisitionFeed):
309 return tag.get_absolute_url()
311 def title(self, tag):
314 def description(self, tag):
315 return u"Spis utworów na stronie http://WolneLektury.pl"
317 def get_object(self, request, slug):
318 return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
320 def items(self, tag):
321 return Book.tagged.with_any([tag])
323 # no class decorators in python 2.5
324 #UserSetFeed = factory_decorator(logged_in_or_basicauth())(UserSetFeed)
328 class SearchFeed(AcquisitionFeed):
329 description = u"Wyniki wyszukiwania na stronie WolneLektury.pl"
330 title = u"Wyniki wyszukiwania"
332 QUOTE_OR_NOT = r'(?:(?=["])"([^"]+)"|([^ ]+))'
333 INLINE_QUERY_RE = re.compile(
334 r"author:" + QUOTE_OR_NOT +
335 "|translator:" + QUOTE_OR_NOT +
336 "|title:" + QUOTE_OR_NOT +
337 "|categories:" + QUOTE_OR_NOT +
338 "|description:" + QUOTE_OR_NOT +
339 "|text:" + QUOTE_OR_NOT
343 'translator': (2, 3),
345 'categories': (6, 7),
346 'description': (8, 9),
352 'translator': 'translators',
354 'categories': 'tag_name_pl',
355 'description': 'text',
359 ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
361 def get_object(self, request):
363 For OPDS 1.1 We should handle a query for search terms
364 and criteria provided either as opensearch or 'inline' query.
365 OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
366 atom:title. Inline query provides author, title, categories (treated as book tags),
367 description (treated as content search terms).
369 if search terms are provided, we shall search for books
370 according to Hint information (from author & contributror & title).
372 but if search terms are empty, we should do a different search
373 (perhaps for is_book=True)
377 query = request.GET.get('q', '')
379 inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
381 remains = re.sub(self.INLINE_QUERY_RE, '', query)
382 remains = re.sub(r'[ \t]+', ' ', remains)
384 def get_criteria(criteria, name):
386 for p in self.MATCHES[name]:
389 return c[p].replace('+', ' ')
394 lambda cn: (cn, get_criteria(inline_criteria, cn)),
395 ['author', 'translator', 'title', 'categories',
396 'description', 'text']))
398 # empty query and text set case?
399 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
401 def remove_dump_data(val):
402 """Some clients don't get opds placeholders and just send them."""
403 if self.ATOM_PLACEHOLDER.match(val):
407 criteria = dict([(cn, remove_dump_data(request.GET.get(cn, '')))
408 for cn in self.MATCHES.keys()])
409 # query is set above.
410 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
414 book_hit_filter = srch.index.Q(book_id__any=True)
415 filters = [book_hit_filter] + [srch.index.Q(
416 **{self.PARAMS_TO_FIELDS.get(cn, cn): criteria[cn]}
417 ) for cn in self.MATCHES.keys() if cn in criteria
421 q = srch.index.query(
423 [srch.index.Q(**{self.PARAMS_TO_FIELDS.get(cn, cn): query})
424 for cn in self.MATCHES.keys()],
427 q = srch.index.query(srch.index.Q())
429 q = srch.apply_filters(q, filters).field_limit(score=True, fields=['book_id'])
430 results = q.execute()
432 book_scores = dict([(r['book_id'], r['score']) for r in results])
433 books = Book.objects.filter(id__in=set([r['book_id'] for r in results]))
435 books.sort(reverse=True, key=lambda book: book_scores[book.id])
438 def get_link(self, query):
439 return "%s?q=%s" % (reverse('search'), query)
441 def items(self, books):