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 urllib.parse 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 from stats.utils import piwik_track
 
  27 log = logging.getLogger('opds')
 
  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)()
 
  72     return urljoin("http://%s" % current_domain, url)
 
  75 class OPDSFeed(Atom1Feed):
 
  76     link_rel = u"subsection"
 
  77     link_type = u"application/atom+xml"
 
  79     _book_parent_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book-parent.png")), str)()
 
  81         _book_parent_img_size = str(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
 
  83         _book_parent_img_size = ''
 
  85     _book_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book.png")), str)()
 
  87         _book_img_size = str(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
 
  91     def add_root_elements(self, handler):
 
  92         super(OPDSFeed, self).add_root_elements(handler)
 
  93         handler.addQuickElement(u"link", None,
 
  94                                 {u"href": reverse("opds_authors"),
 
  96                                  u"type": u"application/atom+xml"})
 
  97         handler.addQuickElement(u"link", None,
 
  98                                 {u"href": full_url(os.path.join(settings.STATIC_URL, "opensearch.xml")),
 
 100                                  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 not item.get('enclosures') is None:
 
 108             handler.addQuickElement(
 
 109                 u"link", u"", {u"href": item['link'], u"rel": u"subsection", u"type": u"application/atom+xml"})
 
 110             # add a "green book" icon
 
 111             handler.addQuickElement(
 
 114                     u"rel": u"http://opds-spec.org/thumbnail",
 
 115                     u"href": self._book_parent_img,
 
 116                     u"length": self._book_parent_img_size,
 
 117                     u"type": u"image/png",
 
 119         if item['pubdate'] is not None:
 
 120             # FIXME: rfc3339_date is undefined, is this ever run?
 
 121             handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
 
 123         # Author information.
 
 124         if item['author_name'] is not None:
 
 125             handler.startElement(u"author", {})
 
 126             handler.addQuickElement(u"name", item['author_name'])
 
 127             if item['author_email'] is not None:
 
 128                 handler.addQuickElement(u"email", item['author_email'])
 
 129             if item['author_link'] is not None:
 
 130                 handler.addQuickElement(u"uri", item['author_link'])
 
 131             handler.endElement(u"author")
 
 134         if item['unique_id'] is not None:
 
 135             unique_id = item['unique_id']
 
 137             # FIXME: get_tag_uri is undefined, is this ever run?
 
 138             unique_id = get_tag_uri(item['link'], item['pubdate'])
 
 139         handler.addQuickElement(u"id", unique_id)
 
 142         # OPDS needs type=text
 
 143         if item['description'] is not None:
 
 144             handler.addQuickElement(u"summary", item['description'], {u"type": u"text"})
 
 146         # Enclosure as OPDS Acquisition Link
 
 147         for enc in item.get('enclosures', []):
 
 148             handler.addQuickElement(
 
 151                     u"rel": u"http://opds-spec.org/acquisition",
 
 153                     u"length": enc.length,
 
 154                     u"type": enc.mime_type,
 
 156             # add a "red book" icon
 
 157             handler.addQuickElement(
 
 160                     u"rel": u"http://opds-spec.org/thumbnail",
 
 161                     u"href": self._book_img,
 
 162                     u"length": self._book_img_size,
 
 163                     u"type": u"image/png",
 
 167         for cat in item['categories']:
 
 168             handler.addQuickElement(u"category", u"", {u"term": cat})
 
 171         if item['item_copyright'] is not None:
 
 172             handler.addQuickElement(u"rights", item['item_copyright'])
 
 175 class AcquisitionFeed(Feed):
 
 177     link = u'http://www.wolnelektury.pl/'
 
 178     item_enclosure_mime_type = "application/epub+zip"
 
 179     author_name = u"Wolne Lektury"
 
 180     author_link = u"http://www.wolnelektury.pl/"
 
 182     def item_title(self, book):
 
 185     def item_description(self):
 
 188     def item_link(self, book):
 
 189         return book.get_absolute_url()
 
 191     def item_author_name(self, book):
 
 193             return book.authors().first().name
 
 194         except AttributeError:
 
 197     def item_author_link(self, book):
 
 199             return book.authors().first().get_absolute_url()
 
 200         except AttributeError:
 
 203     def item_enclosure_url(self, book):
 
 204         return full_url(book.epub_url()) if book.epub_file else None
 
 206     def item_enclosure_length(self, book):
 
 207         return book.epub_file.size if book.epub_file else None
 
 211 class RootFeed(Feed):
 
 213     title = u'Wolne Lektury'
 
 214     link = u'http://wolnelektury.pl/'
 
 215     description = u"Spis utworów na stronie http://WolneLektury.pl"
 
 216     author_name = u"Wolne Lektury"
 
 217     author_link = u"http://wolnelektury.pl/"
 
 222     def item_title(self, item):
 
 225     def item_link(self, item):
 
 226         return reverse(item['link'], args=item['link_args'])
 
 228     def item_description(self, item):
 
 229         return item['description']
 
 233 class ByCategoryFeed(Feed):
 
 235     link = u'http://wolnelektury.pl/'
 
 236     description = u"Spis utworów na stronie http://WolneLektury.pl"
 
 237     author_name = u"Wolne Lektury"
 
 238     author_link = u"http://wolnelektury.pl/"
 
 240     def get_object(self, request, category):
 
 241         feed = [feed for feed in _root_feeds if feed['category'] == category]
 
 249     def title(self, feed):
 
 252     def items(self, feed):
 
 253         return Tag.objects.filter(category=feed['category']).exclude(items=None)
 
 255     def item_title(self, item):
 
 258     def item_link(self, item):
 
 259         return reverse("opds_by_tag", args=[item.category, item.slug])
 
 261     def item_description(self):
 
 266 class ByTagFeed(AcquisitionFeed):
 
 268         return tag.get_absolute_url()
 
 270     def title(self, tag):
 
 273     def description(self, tag):
 
 274         return u"Spis utworów na stronie http://WolneLektury.pl"
 
 276     def get_object(self, request, category, slug):
 
 277         return get_object_or_404(Tag, category=category, slug=slug)
 
 279     def items(self, tag):
 
 280         return Book.tagged_top_level([tag])
 
 283 @factory_decorator(logged_in_or_basicauth())
 
 285 class UserFeed(Feed):
 
 287     link = u'http://www.wolnelektury.pl/'
 
 288     description = u"Półki użytkownika na stronie http://WolneLektury.pl"
 
 289     author_name = u"Wolne Lektury"
 
 290     author_link = u"http://wolnelektury.pl/"
 
 292     def get_object(self, request):
 
 295     def title(self, user):
 
 296         return u"Półki użytkownika %s" % user.username
 
 298     def items(self, user):
 
 299         return Tag.objects.filter(category='set', user=user).exclude(items=None)
 
 301     def item_title(self, item):
 
 304     def item_link(self, item):
 
 305         return reverse("opds_user_set", args=[item.slug])
 
 307     def item_description(self):
 
 311 @factory_decorator(logged_in_or_basicauth())
 
 313 class UserSetFeed(AcquisitionFeed):
 
 315         return tag.get_absolute_url()
 
 317     def title(self, tag):
 
 320     def description(self, tag):
 
 321         return u"Spis utworów na stronie http://WolneLektury.pl"
 
 323     def get_object(self, request, slug):
 
 324         return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
 
 326     def items(self, tag):
 
 327         return Book.tagged.with_any([tag])
 
 331 class SearchFeed(AcquisitionFeed):
 
 332     description = u"Wyniki wyszukiwania na stronie WolneLektury.pl"
 
 333     title = u"Wyniki wyszukiwania"
 
 335     QUOTE_OR_NOT = r'(?:(?=["])"([^"]+)"|([^ ]+))'
 
 336     INLINE_QUERY_RE = re.compile(
 
 337         r"author:" + QUOTE_OR_NOT +
 
 338         "|translator:" + QUOTE_OR_NOT +
 
 339         "|title:" + QUOTE_OR_NOT +
 
 340         "|categories:" + QUOTE_OR_NOT +
 
 341         "|description:" + QUOTE_OR_NOT +
 
 342         "|text:" + QUOTE_OR_NOT
 
 346         'translator': (2, 3),
 
 348         'categories': (6, 7),
 
 349         'description': (8, 9),
 
 355         'translator': 'translators',
 
 357         'categories': 'tag_name_pl',
 
 358         'description': 'text',
 
 362     ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
 
 364     def get_object(self, request):
 
 366         For OPDS 1.1 We should handle a query for search terms
 
 367         and criteria provided either as opensearch or 'inline' query.
 
 368         OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
 
 369         atom:title. Inline query provides author, title, categories (treated as book tags),
 
 370         description (treated as content search terms).
 
 372         if search terms are provided, we shall search for books
 
 373         according to Hint information (from author & contributror & title).
 
 375         but if search terms are empty, we should do a different search
 
 376         (perhaps for is_book=True)
 
 380         query = request.GET.get('q', '')
 
 382         inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
 
 384             remains = re.sub(self.INLINE_QUERY_RE, '', query)
 
 385             remains = re.sub(r'[ \t]+', ' ', remains)
 
 387             def get_criteria(criteria, name):
 
 389                     for p in self.MATCHES[name]:
 
 392                                 return c[p].replace('+', ' ')
 
 397                 lambda cn: (cn, get_criteria(inline_criteria, cn)),
 
 398                 ['author', 'translator', 'title', 'categories',
 
 399                  'description', 'text']))
 
 401             # empty query and text set case?
 
 402             log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
 
 404             def remove_dump_data(val):
 
 405                 """Some clients don't get opds placeholders and just send them."""
 
 406                 if self.ATOM_PLACEHOLDER.match(val):
 
 411                 (cn, remove_dump_data(request.GET.get(cn, '')))
 
 412                 for cn in self.MATCHES.keys())
 
 413             # query is set above.
 
 414             log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
 
 418         book_hit_filter = srch.index.Q(book_id__any=True)
 
 419         filters = [book_hit_filter] + [srch.index.Q(
 
 420             **{self.PARAMS_TO_FIELDS.get(cn, cn): criteria[cn]}
 
 421             ) for cn in self.MATCHES.keys() if cn in criteria
 
 425             q = srch.index.query(
 
 428                     [srch.index.Q(**{self.PARAMS_TO_FIELDS.get(cn, cn): query}) for cn in self.MATCHES.keys()],
 
 431             q = srch.index.query(srch.index.Q())
 
 433         q = srch.apply_filters(q, filters).field_limit(score=True, fields=['book_id'])
 
 434         results = q.execute()
 
 436         book_scores = dict([(r['book_id'], r['score']) for r in results])
 
 437         books = Book.objects.filter(id__in=set([r['book_id'] for r in results]))
 
 439         books.sort(reverse=True, key=lambda book: book_scores[book.id])
 
 442     def get_link(self, query):
 
 443         return "%s?q=%s" % (reverse('search'), query)
 
 445     def items(self, books):