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     QUOTE_OR_NOT = r'(?:(?=["])"([^"]+)"|([^ ]+))'
 
 332     INLINE_QUERY_RE = re.compile(
 
 333         r"author:" + QUOTE_OR_NOT +
 
 334         "|translator:" + QUOTE_OR_NOT +
 
 335         "|title:" + QUOTE_OR_NOT +
 
 336         "|categories:" + QUOTE_OR_NOT +
 
 337         "|description:" + QUOTE_OR_NOT +
 
 338         "|text:" + QUOTE_OR_NOT
 
 342         'translator': (2, 3),
 
 344         'categories': (6, 7),
 
 345         'description': (8, 9),
 
 351         'translator': 'translators',
 
 353         'categories': 'tag_name_pl',
 
 354         'description': 'text',
 
 358     ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
 
 360     def get_object(self, request):
 
 362         For OPDS 1.1 We should handle a query for search terms
 
 363         and criteria provided either as opensearch or 'inline' query.
 
 364         OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
 
 365         atom:title. Inline query provides author, title, categories (treated as book tags),
 
 366         description (treated as content search terms).
 
 368         if search terms are provided, we shall search for books
 
 369         according to Hint information (from author & contributror & title).
 
 371         but if search terms are empty, we should do a different search
 
 372         (perhaps for is_book=True)
 
 376         query = request.GET.get('q', '')
 
 378         inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
 
 380             remains = re.sub(self.INLINE_QUERY_RE, '', query)
 
 381             remains = re.sub(r'[ \t]+', ' ', remains)
 
 383             def get_criteria(criteria, name):
 
 385                     for p in self.MATCHES[name]:
 
 388                                 return c[p].replace('+', ' ')
 
 393                 lambda cn: (cn, get_criteria(inline_criteria, cn)),
 
 394                 ['author', 'translator', 'title', 'categories',
 
 395                  'description', 'text']))
 
 397             # empty query and text set case?
 
 398             log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
 
 400             def remove_dump_data(val):
 
 401                 """Some clients don't get opds placeholders and just send them."""
 
 402                 if self.ATOM_PLACEHOLDER.match(val):
 
 406             criteria = dict([(cn, remove_dump_data(request.GET.get(cn, '')))
 
 407                         for cn in self.MATCHES.keys()])
 
 408             # query is set above.
 
 409             log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
 
 413         book_hit_filter = srch.index.Q(book_id__any=True)
 
 414         filters = [book_hit_filter] + [srch.index.Q(
 
 415             **{self.PARAMS_TO_FIELDS.get(cn, cn): criteria[cn]}
 
 416             ) for cn in self.MATCHES.keys() if cn in criteria
 
 420             q = srch.index.query(
 
 422                        [srch.index.Q(**{self.PARAMS_TO_FIELDS.get(cn, cn): query})
 
 423                         for cn in self.MATCHES.keys()],
 
 426             q = srch.index.query(srch.index.Q())
 
 428         q = srch.apply_filters(q, filters).field_limit(score=True, fields=['book_id'])
 
 429         results = q.execute()
 
 431         book_scores = dict([(r['book_id'], r['score']) for r in results])
 
 432         books = Book.objects.filter(id__in=set([r['book_id'] for r in results]))
 
 434         books.sort(reverse=True, key=lambda book: book_scores[book.id])
 
 437     def get_link(self, query):
 
 438         return "%s?q=%s" % (reverse('search'), query)
 
 440     def items(self, books):