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")), str)()
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")), str)()
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(items=None)
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 return Book.tagged_top_level([tag])
270 @factory_decorator(logged_in_or_basicauth())
272 class UserFeed(Feed):
274 link = u'http://www.wolnelektury.pl/'
275 description = u"Półki użytkownika na stronie http://WolneLektury.pl"
276 author_name = u"Wolne Lektury"
277 author_link = u"http://wolnelektury.pl/"
279 def get_object(self, request):
282 def title(self, user):
283 return u"Półki użytkownika %s" % user.username
285 def items(self, user):
286 return Tag.objects.filter(category='set', user=user).exclude(items=None)
288 def item_title(self, item):
291 def item_link(self, item):
292 return reverse("opds_user_set", args=[item.slug])
294 def item_description(self):
298 @factory_decorator(logged_in_or_basicauth())
300 class UserSetFeed(AcquisitionFeed):
302 return tag.get_absolute_url()
304 def title(self, tag):
307 def description(self, tag):
308 return u"Spis utworów na stronie http://WolneLektury.pl"
310 def get_object(self, request, slug):
311 return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
313 def items(self, tag):
314 return Book.tagged.with_any([tag])
318 class SearchFeed(AcquisitionFeed):
319 description = u"Wyniki wyszukiwania na stronie WolneLektury.pl"
320 title = u"Wyniki wyszukiwania"
322 QUOTE_OR_NOT = r'(?:(?=["])"([^"]+)"|([^ ]+))'
323 INLINE_QUERY_RE = re.compile(
324 r"author:" + QUOTE_OR_NOT +
325 "|translator:" + QUOTE_OR_NOT +
326 "|title:" + QUOTE_OR_NOT +
327 "|categories:" + QUOTE_OR_NOT +
328 "|description:" + QUOTE_OR_NOT +
329 "|text:" + QUOTE_OR_NOT
333 'translator': (2, 3),
335 'categories': (6, 7),
336 'description': (8, 9),
342 'translator': 'translators',
344 'categories': 'tag_name_pl',
345 'description': 'text',
349 ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
351 def get_object(self, request):
353 For OPDS 1.1 We should handle a query for search terms
354 and criteria provided either as opensearch or 'inline' query.
355 OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
356 atom:title. Inline query provides author, title, categories (treated as book tags),
357 description (treated as content search terms).
359 if search terms are provided, we shall search for books
360 according to Hint information (from author & contributror & title).
362 but if search terms are empty, we should do a different search
363 (perhaps for is_book=True)
367 query = request.GET.get('q', '')
369 inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
371 remains = re.sub(self.INLINE_QUERY_RE, '', query)
372 remains = re.sub(r'[ \t]+', ' ', remains)
374 def get_criteria(criteria, name):
376 for p in self.MATCHES[name]:
379 return c[p].replace('+', ' ')
384 lambda cn: (cn, get_criteria(inline_criteria, cn)),
385 ['author', 'translator', 'title', 'categories',
386 'description', 'text']))
388 # empty query and text set case?
389 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
391 def remove_dump_data(val):
392 """Some clients don't get opds placeholders and just send them."""
393 if self.ATOM_PLACEHOLDER.match(val):
397 criteria = dict([(cn, remove_dump_data(request.GET.get(cn, '')))
398 for cn in self.MATCHES.keys()])
399 # query is set above.
400 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
404 book_hit_filter = srch.index.Q(book_id__any=True)
405 filters = [book_hit_filter] + [srch.index.Q(
406 **{self.PARAMS_TO_FIELDS.get(cn, cn): criteria[cn]}
407 ) for cn in self.MATCHES.keys() if cn in criteria
411 q = srch.index.query(
413 [srch.index.Q(**{self.PARAMS_TO_FIELDS.get(cn, cn): query})
414 for cn in self.MATCHES.keys()],
417 q = srch.index.query(srch.index.Q())
419 q = srch.apply_filters(q, filters).field_limit(score=True, fields=['book_id'])
420 results = q.execute()
422 book_scores = dict([(r['book_id'], r['score']) for r in results])
423 books = Book.objects.filter(id__in=set([r['book_id'] for r in results]))
425 books.sort(reverse=True, key=lambda book: book_scores[book.id])
428 def get_link(self, query):
429 return "%s?q=%s" % (reverse('search'), query)
431 def items(self, books):