+
+def _no_diacritics_regexp(query):
+ """ returns a regexp for searching for a query without diacritics
+
+ should be locale-aware """
+ names = {
+ u'a':u'aąĄ', u'c':u'cćĆ', u'e':u'eęĘ', u'l': u'lłŁ', u'n':u'nńŃ', u'o':u'oóÓ', u's':u'sśŚ', u'z':u'zźżŹŻ',
+ u'ą':u'ąĄ', u'ć':u'ćĆ', u'ę':u'ęĘ', u'ł': u'łŁ', u'ń':u'ńŃ', u'ó':u'óÓ', u'ś':u'śŚ', u'ź':u'źŹ', u'ż':u'żŻ'
+ }
+ def repl(m):
+ l = m.group()
+ return u"(%s)" % '|'.join(names[l])
+ return re.sub(u'[%s]' % (u''.join(names.keys())), repl, query)
+
+def unicode_re_escape(query):
+ """ Unicode-friendly version of re.escape """
+ return re.sub('(?u)(\W)', r'\\\1', query)
+
+def _word_starts_with(name, prefix):
+ """returns a Q object getting models having `name` contain a word
+ starting with `prefix`
+
+ We define word characters as alphanumeric and underscore, like in JS.
+
+ Works for MySQL, PostgreSQL, Oracle.
+ For SQLite, _sqlite* version is substituted for this.
+ """
+ kwargs = {}
+
+ prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
+ # can't use [[:<:]] (word start),
+ # but we want both `xy` and `(xy` to catch `(xyz)`
+ kwargs['%s__iregex' % name] = u"(^|[^[:alnum:]_])%s" % prefix
+
+ return Q(**kwargs)
+
+
+def _word_starts_with_regexp(prefix):
+ prefix = _no_diacritics_regexp(unicode_re_escape(prefix))
+ return ur"(^|(?<=[^\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]))%s" % prefix
+
+
+def _sqlite_word_starts_with(name, prefix):
+ """ version of _word_starts_with for SQLite
+
+ SQLite in Django uses Python re module
+ """
+ kwargs = {}
+ kwargs['%s__iregex' % name] = _word_starts_with_regexp(prefix)
+ return Q(**kwargs)
+
+
+if hasattr(settings, 'DATABASES'):
+ if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
+ _word_starts_with = _sqlite_word_starts_with
+elif settings.DATABASE_ENGINE == 'sqlite3':
+ _word_starts_with = _sqlite_word_starts_with
+
+
+class App():
+ def __init__(self, name, view):
+ self.name = name
+ self._view = view
+ self.lower = name.lower()
+ self.category = 'application'
+ def view(self):
+ return reverse(*self._view)
+
+_apps = (
+ App(u'Leśmianator', (u'lesmianator', )),
+ )
+
+
+def _tags_starting_with(prefix, user=None):
+ prefix = prefix.lower()
+ # PD counter
+ book_stubs = pdcounter_models.BookStub.objects.filter(_word_starts_with('title', prefix))
+ authors = pdcounter_models.Author.objects.filter(_word_starts_with('name', prefix))
+
+ books = models.Book.objects.filter(_word_starts_with('title', prefix))
+ tags = models.Tag.objects.filter(_word_starts_with('name', prefix))
+ if user and user.is_authenticated():
+ tags = tags.filter(~Q(category='book') & (~Q(category='set') | Q(user=user)))
+ else:
+ tags = tags.filter(~Q(category='book') & ~Q(category='set'))
+
+ prefix_regexp = re.compile(_word_starts_with_regexp(prefix))
+ return list(books) + list(tags) + [app for app in _apps if prefix_regexp.search(app.lower)] + list(book_stubs) + list(authors)
+
+
+def _get_result_link(match, tag_list):
+ if isinstance(match, models.Tag):
+ return reverse('catalogue.views.tagged_object_list',
+ kwargs={'tags': '/'.join(tag.url_chunk for tag in tag_list + [match])}
+ )
+ elif isinstance(match, App):
+ return match.view()
+ else:
+ return match.get_absolute_url()
+
+
+def _get_result_type(match):
+ if isinstance(match, models.Book) or isinstance(match, pdcounter_models.BookStub):
+ type = 'book'
+ else:
+ type = match.category
+ return type
+
+
+def books_starting_with(prefix):
+ prefix = prefix.lower()
+ return models.Book.objects.filter(_word_starts_with('title', prefix))
+
+
+def find_best_matches(query, user=None):
+ """ Finds a models.Book, Tag, models.BookStub or Author best matching a query.
+
+ Returns a with:
+ - zero elements when nothing is found,
+ - one element when a best result is found,
+ - more then one element on multiple exact matches
+
+ Raises a ValueError on too short a query.
+ """
+
+ query = query.lower()
+ if len(query) < 2:
+ raise ValueError("query must have at least two characters")
+
+ result = tuple(_tags_starting_with(query, user))
+ # remove pdcounter stuff
+ book_titles = set(match.pretty_title().lower() for match in result
+ if isinstance(match, models.Book))
+ authors = set(match.name.lower() for match in result
+ if isinstance(match, models.Tag) and match.category == 'author')
+ result = tuple(res for res in result if not (
+ (isinstance(res, pdcounter_models.BookStub) and res.pretty_title().lower() in book_titles)
+ or (isinstance(res, pdcounter_models.Author) and res.name.lower() in authors)
+ ))
+
+ exact_matches = tuple(res for res in result if res.name.lower() == query)
+ if exact_matches:
+ return exact_matches
+ else:
+ return tuple(result)[:1]
+
+