From: Marcin Koziej Date: Thu, 24 Nov 2011 16:10:14 +0000 (+0100) Subject: Search in stagin phase - waiting for final guidelines on new wl X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/5c959cbb46c29a03cb6c8bc0e8b5aae5765bf150 Search in stagin phase - waiting for final guidelines on new wl Search integrated into tagbook context thrown out blockjoins --- diff --git a/apps/catalogue/fields.py b/apps/catalogue/fields.py index 048824498..57ce58189 100644 --- a/apps/catalogue/fields.py +++ b/apps/catalogue/fields.py @@ -73,20 +73,12 @@ class JSONField(models.TextField): class JQueryAutoCompleteWidget(forms.TextInput): - def __init__(self, source, options=None, *args, **kwargs): - self.source = source - self.options = None - if options: - self.options = dumps(options) + def __init__(self, options, *args, **kwargs): + self.options = dumps(options) super(JQueryAutoCompleteWidget, self).__init__(*args, **kwargs) - def render_js(self, field_id): - source = "'%s'" % escape(self.source) - options = '' - if self.options: - options += ', %s' % self.options - - return u'$(\'#%s\').autocomplete(%s%s).result(autocomplete_result_handler);' % (field_id, source, options) + def render_js(self, field_id, options): + return u'$(\'#%s\').autocomplete(%s).result(autocomplete_result_handler);' % (field_id, options) def render(self, name, value=None, attrs=None): final_attrs = self.build_attrs(attrs, name=name) @@ -100,21 +92,47 @@ class JQueryAutoCompleteWidget(forms.TextInput): ''' % { - 'attrs' : flatatt(final_attrs), - 'js' : self.render_js(final_attrs['id']), + 'attrs': flatatt(final_attrs), + 'js' : self.render_js(final_attrs['id'], self.options), } return mark_safe(html) +class JQueryAutoCompleteSearchWidget(JQueryAutoCompleteWidget): + def __init__(self, *args, **kwargs): + super(JQueryAutoCompleteSearchWidget, self).__init__(*args, **kwargs) + + def render_js(self, field_id, options): + return u""" + $('#%s') + .autocomplete($.extend({ + minLength: 0, + select: autocomplete_result_handler, + focus: function (ui, item) { return false; } + }, %s)) + .data("autocomplete")._renderItem = autocomplete_format_item; + """ % (field_id, options) + + class JQueryAutoCompleteField(forms.CharField): - def __init__(self, source, options=None, *args, **kwargs): + def __init__(self, source, options={}, *args, **kwargs): if 'widget' not in kwargs: - kwargs['widget'] = JQueryAutoCompleteWidget(source, options) + options['source'] = source + kwargs['widget'] = JQueryAutoCompleteWidget(options) super(JQueryAutoCompleteField, self).__init__(*args, **kwargs) +class JQueryAutoCompleteSearchField(forms.CharField): + def __init__(self, source, options={}, *args, **kwargs): + if 'widget' not in kwargs: + options['source'] = source + kwargs['widget'] = JQueryAutoCompleteSearchWidget(options) + + super(JQueryAutoCompleteSearchField, self).__init__(*args, **kwargs) + + class OverwritingFieldFile(FieldFile): """ Deletes the old file before saving the new one. diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 04969c2e2..92f50edcb 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from slughifi import slughifi from catalogue.models import Tag, Book -from catalogue.fields import JQueryAutoCompleteField +from catalogue.fields import JQueryAutoCompleteSearchField from catalogue import utils @@ -30,15 +30,20 @@ class BookImportForm(forms.Form): class SearchForm(forms.Form): - q = JQueryAutoCompleteField('/katalog/tags/', {'minChars': 2, 'selectFirst': True, 'cacheLength': 50, 'matchContains': "word"}) + q = JQueryAutoCompleteSearchField('/newsearch/hint/') # {'minChars': 2, 'selectFirst': True, 'cacheLength': 50, 'matchContains': "word"}) tags = forms.CharField(widget=forms.HiddenInput, required=False) + book = forms.IntegerField(widget=forms.HiddenInput, min_value=0, required=False) + def __init__(self, *args, **kwargs): tags = kwargs.pop('tags', []) + book = kwargs.pop('book', None) super(SearchForm, self).__init__(*args, **kwargs) - self.fields['q'].widget.attrs['title'] = _('title, author, theme/topic, epoch, kind, genre') + self.fields['q'].widget.attrs['title'] = _('title, author, theme/topic, epoch, kind, genre, phrase') #self.fields['q'].widget.attrs['style'] = '' self.fields['tags'].initial = '/'.join(tag.url_chunk for tag in Tag.get_tag_list(tags)) + if book is not None: + self.fields['book'].initial = book.id class UserSetsForm(forms.Form): diff --git a/apps/catalogue/management/commands/__init__.py b/apps/catalogue/management/commands/__init__.py index e69de29bb..8d1c8b69c 100644 --- a/apps/catalogue/management/commands/__init__.py +++ b/apps/catalogue/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index c770892d9..5d623fa92 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -20,7 +20,7 @@ urlpatterns = patterns('catalogue.views', url(r'^polki/nowa/$', 'new_set', name='new_set'), url(r'^tags/$', 'tags_starting_with', name='hint'), url(r'^jtags/$', 'json_tags_starting_with', name='jhint'), - url(r'^szukaj/$', 'search', name='search'), + url(r'^szukaj/$', 'search', name='old_search'), # zip #url(r'^zip/pdf\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'), diff --git a/apps/search/__init__.py b/apps/search/__init__.py index a3f8e9ec0..1b12eb7b0 100644 --- a/apps/search/__init__.py +++ b/apps/search/__init__.py @@ -1,3 +1,3 @@ import lucene -from index import Index, Search, ReusableIndex, MultiSearch, SearchResult, JVM +from index import Index, Search, ReusableIndex, MultiSearch, SearchResult, JVM, IndexChecker diff --git a/apps/search/index.py b/apps/search/index.py index af3dd9a60..9b7efa2b6 100644 --- a/apps/search/index.py +++ b/apps/search/index.py @@ -1,21 +1,27 @@ # -*- coding: utf-8 -*- from django.conf import settings -from lucene import SimpleFSDirectory, IndexWriter, File, Field, \ +from lucene import SimpleFSDirectory, IndexWriter, CheckIndex, \ + File, Field, Integer, \ NumericField, Version, Document, JavaError, IndexSearcher, \ QueryParser, PerFieldAnalyzerWrapper, \ SimpleAnalyzer, PolishAnalyzer, ArrayList, \ - KeywordAnalyzer, NumericRangeQuery, BooleanQuery, \ - BlockJoinQuery, BlockJoinCollector, TermsFilter, \ + KeywordAnalyzer, NumericRangeQuery, NumericRangeFilter, BooleanQuery, \ + BlockJoinQuery, BlockJoinCollector, Filter, TermsFilter, ChainedFilter, \ HashSet, BooleanClause, Term, CharTermAttribute, \ - PhraseQuery, MultiPhraseQuery, StringReader, TermQuery, BlockJoinQuery, \ - FuzzyQuery, FuzzyTermEnum, Sort, Integer, \ + PhraseQuery, MultiPhraseQuery, StringReader, TermQuery, \ + FuzzyQuery, FuzzyTermEnum, PrefixTermEnum, Sort, Integer, \ SimpleHTMLFormatter, Highlighter, QueryScorer, TokenSources, TextFragment, \ - initVM, CLASSPATH, JArray + BooleanFilter, TermsFilter, FilterClause, QueryWrapperFilter, \ + initVM, CLASSPATH, JArray, JavaError # KeywordAnalyzer + +# Initialize jvm JVM = initVM(CLASSPATH) + import sys import os +import re import errno from librarian import dcparser from librarian.parser import WLDocument @@ -29,8 +35,13 @@ import traceback class WLAnalyzer(PerFieldAnalyzerWrapper): def __init__(self): polish = PolishAnalyzer(Version.LUCENE_34) + # polish_gap.setPositionIncrementGap(999) + simple = SimpleAnalyzer(Version.LUCENE_34) + # simple_gap.setPositionIncrementGap(999) + keyword = KeywordAnalyzer(Version.LUCENE_34) + # not sure if needed: there's NOT_ANALYZED meaning basically the same PerFieldAnalyzerWrapper.__init__(self, polish) @@ -45,9 +56,15 @@ class WLAnalyzer(PerFieldAnalyzerWrapper): self.addAnalyzer("author", simple) self.addAnalyzer("is_book", keyword) + self.addAnalyzer("themes", simple) + self.addAnalyzer("themes_pl", polish) + + self.addAnalyzer("tag_name", simple) + self.addAnalyzer("tag_name_pl", polish) + self.addAnalyzer("KEYWORD", keyword) self.addAnalyzer("SIMPLE", simple) - self.addAnalyzer("NATURAL", polish) + self.addAnalyzer("POLISH", polish) class IndexStore(object): @@ -64,6 +81,55 @@ class IndexStore(object): else: raise +class IndexChecker(IndexStore): + def __init__(self): + IndexStore.__init__(self) + + def check(self): + checker = CheckIndex(self.store) + status = checker.checkIndex() + return status + + +class Snippets(object): + SNIPPET_DIR = "snippets" + + def __init__(self, book_id): + try: + os.makedirs(os.path.join(settings.SEARCH_INDEX, self.SNIPPET_DIR)) + except OSError as exc: + if exc.errno == errno.EEXIST: + pass + else: raise + self.book_id = book_id + self.file = None + + def open(self, mode='r'): + if not 'b' in mode: + mode += 'b' + self.file = open(os.path.join(settings.SEARCH_INDEX, self.SNIPPET_DIR, str(self.book_id)), mode) + self.position = 0 + return self + + def add(self, snippet): + txt = snippet.encode('utf-8') + l = len(txt) + self.file.write(txt) + pos = (self.position, l) + self.position += l + print "Snip<%s>%s" %(pos, txt) + return pos + + def get(self, pos): + self.file.seek(pos[0], 0) + txt = self.file.read(pos[1]).decode('utf-8') + print "got from snippets %d bytes from %s:" % (len(txt), pos) + return txt + + def close(self): + self.file.close() + + class Index(IndexStore): def __init__(self, analyzer=None): IndexStore.__init__(self) @@ -79,11 +145,30 @@ class Index(IndexStore): IndexWriter.MaxFieldLength.LIMITED) return self.index - def close(self): + def optimize(self): self.index.optimize() + + def close(self): + try: + self.index.optimize() + except JavaError, je: + print "Error during optimize phase, check index: %s" % je + self.index.close() self.index = None + def index_tags(self): + q = NumericRangeQuery.newIntRange("tag_id", 0, Integer.MAX_VALUE, True, True) + self.index.deleteDocuments(q) + + for tag in catalogue.models.Tag.objects.all(): + doc = Document() + doc.add(NumericField("tag_id", Field.Store.YES, True).setIntValue(tag.id)) + doc.add(Field("tag_name", tag.name, Field.Store.NO, Field.Index.ANALYZED)) + doc.add(Field("tag_name_pl", tag.name, Field.Store.NO, Field.Index.ANALYZED)) + doc.add(Field("tag_category", tag.category, Field.Store.NO, Field.Index.NOT_ANALYZED)) + self.index.addDocument(doc) + def remove_book(self, book): q = NumericRangeQuery.newIntRange("book_id", book.id, book.id, True, True) self.index.deleteDocuments(q) @@ -92,14 +177,19 @@ class Index(IndexStore): if overwrite: self.remove_book(book) - doc = self.extract_metadata(book) - parts = self.extract_content(book) - block = ArrayList().of_(Document) + book_doc = self.create_book_doc(book) + meta_fields = self.extract_metadata(book) + for f in meta_fields.values(): + if isinstance(f, list) or isinstance(f, tuple): + for elem in f: + book_doc.add(elem) + else: + book_doc.add(f) + + self.index.addDocument(book_doc) + del book_doc - for p in parts: - block.add(p) - block.add(doc) - self.index.addDocuments(block) + self.index_content(book, book_fields=[meta_fields['title'], meta_fields['author']]) master_tags = [ 'opowiadanie', @@ -123,14 +213,14 @@ class Index(IndexStore): return doc def extract_metadata(self, book): + fields = {} book_info = dcparser.parse(book.xml_file) print("extract metadata for book %s id=%d, thread%d" % (book.slug, book.id, current_thread().ident)) - doc = self.create_book_doc(book) - doc.add(Field("slug", book.slug, Field.Store.NO, Field.Index.ANALYZED_NO_NORMS)) - doc.add(Field("tags", ','.join([t.name for t in book.tags]), Field.Store.NO, Field.Index.ANALYZED)) - doc.add(Field("is_book", 'true', Field.Store.NO, Field.Index.NOT_ANALYZED)) + fields['slug'] = Field("slug", book.slug, Field.Store.NO, Field.Index.ANALYZED_NO_NORMS) + fields['tags'] = self.add_gaps([Field("tags", t.name, Field.Store.NO, Field.Index.ANALYZED) for t in book.tags], 'tags') + fields['is_book'] = Field("is_book", 'true', Field.Store.NO, Field.Index.NOT_ANALYZED) # validator, name for field in dcparser.BookInfo.FIELDS: @@ -144,7 +234,7 @@ class Index(IndexStore): if field.multiple: s = ', '.join(s) try: - doc.add(Field(field.name, s, Field.Store.NO, Field.Index.ANALYZED)) + fields[field.name] = Field(field.name, s, Field.Store.NO, Field.Index.ANALYZED) except JavaError as je: raise Exception("failed to add field: %s = '%s', %s(%s)" % (field.name, s, je.message, je.args)) elif type_indicator == dcparser.as_person: @@ -153,40 +243,32 @@ class Index(IndexStore): persons = unicode(p) else: persons = ', '.join(map(unicode, p)) - doc.add(Field(field.name, persons, Field.Store.NO, Field.Index.ANALYZED)) + fields[field.name] = Field(field.name, persons, Field.Store.NO, Field.Index.ANALYZED) elif type_indicator == dcparser.as_date: dt = getattr(book_info, field.name) - doc.add(Field(field.name, "%04d%02d%02d" % (dt.year, dt.month, dt.day), Field.Store.NO, Field.Index.NOT_ANALYZED)) - return doc + fields[field.name] = Field(field.name, "%04d%02d%02d" %\ + (dt.year, dt.month, dt.day), Field.Store.NO, Field.Index.NOT_ANALYZED) + return fields def get_master(self, root): for master in root.iter(): if master.tag in self.master_tags: return master - def extract_content(self, book): + def add_gaps(self, fields, fieldname): + def gap(): + while True: + yield Field(fieldname, ' ', Field.Store.NO, Field.Index.NOT_ANALYZED) + return reduce(lambda a, b: a + b, zip(fields, gap()))[0:-1] + + def index_content(self, book, book_fields=[]): wld = WLDocument.from_file(book.xml_file.path) root = wld.edoc.getroot() - # first we build a sequence of top-level items. - # book_id - # header_index - the 0-indexed position of header element. - # content master = self.get_master(root) if master is None: return [] - header_docs = [] - for header, position in zip(list(master), range(len(master))): - if header.tag in self.skip_header_tags: - continue - doc = self.create_book_doc(book) - doc.add(NumericField("header_index", Field.Store.YES, True).setIntValue(position)) - doc.add(Field("header_type", header.tag, Field.Store.YES, Field.Index.NOT_ANALYZED)) - content = u' '.join([t for t in header.itertext()]) - doc.add(Field("content", content, Field.Store.YES, Field.Index.ANALYZED)) - header_docs.append(doc) - def walker(node): yield node, None for child in list(node): @@ -195,52 +277,100 @@ class Index(IndexStore): yield None, node return - # Then we create a document for each fragments - # fragment_anchor - the anchor - # themes - list of themes [not indexed] - fragment_docs = [] - # will contain (framgent id -> { content: [], themes: [] } - fragments = {} - for start, end in walker(master): - if start is not None and start.tag == 'begin': - fid = start.attrib['id'][1:] - fragments[fid] = {'content': [], 'themes': []} - fragments[fid]['content'].append(start.tail) - elif start is not None and start.tag == 'motyw': - fid = start.attrib['id'][1:] - fragments[fid]['themes'].append(start.text) - fragments[fid]['content'].append(start.tail) - elif start is not None and start.tag == 'end': - fid = start.attrib['id'][1:] - if fid not in fragments: - continue # a broken node, skip it - frag = fragments[fid] - del fragments[fid] - - def jstr(l): - return u' '.join(map( - lambda x: x == None and u'(none)' or unicode(x), - l)) - - doc = self.create_book_doc(book) - doc.add(Field("fragment_anchor", fid, + def fix_format(text): + return re.sub("/$", "", text, flags=re.M) + + def add_part(snippets, **fields): + doc = self.create_book_doc(book) + for f in book_fields: + doc.add(f) + + doc.add(NumericField('header_index', Field.Store.YES, True).setIntValue(fields["header_index"])) + doc.add(NumericField("header_span", Field.Store.YES, True)\ + .setIntValue('header_span' in fields and fields['header_span'] or 1)) + doc.add(Field('header_type', fields["header_type"], Field.Store.YES, Field.Index.NOT_ANALYZED)) + + doc.add(Field('content', fields["content"], Field.Store.NO, Field.Index.ANALYZED, \ + Field.TermVector.WITH_POSITIONS_OFFSETS)) + + snip_pos = snippets.add(fields["content"]) + doc.add(NumericField("snippets_position", Field.Store.YES, True).setIntValue(snip_pos[0])) + doc.add(NumericField("snippets_length", Field.Store.YES, True).setIntValue(snip_pos[1])) + + if 'fragment_anchor' in fields: + doc.add(Field("fragment_anchor", fields['fragment_anchor'], Field.Store.YES, Field.Index.NOT_ANALYZED)) - doc.add(Field("content", - u' '.join(filter(lambda s: s is not None, frag['content'])), - Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)) - doc.add(Field("themes", - u' '.join(filter(lambda s: s is not None, frag['themes'])), - Field.Store.NO, Field.Index.ANALYZED)) - - fragment_docs.append(doc) - elif start is not None: - for frag in fragments.values(): - frag['content'].append(start.text) - elif end is not None: - for frag in fragments.values(): - frag['content'].append(end.tail) - - return header_docs + fragment_docs + + if 'themes' in fields: + themes, themes_pl = zip(*[ + (Field("themes", theme, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS), + Field("themes_pl", theme, Field.Store.NO, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS)) + for theme in fields['themes']]) + + themes = self.add_gaps(themes, 'themes') + themes_pl = self.add_gaps(themes_pl, 'themes_pl') + + for t in themes: + doc.add(t) + for t in themes_pl: + doc.add(t) + + return doc + + fragments = {} + snippets = Snippets(book.id).open('w') + try: + for header, position in zip(list(master), range(len(master))): + + if header.tag in self.skip_header_tags: + continue + + content = u' '.join([t for t in header.itertext()]) + content = fix_format(content) + + doc = add_part(snippets, header_index=position, header_type=header.tag, content=content) + + self.index.addDocument(doc) + + for start, end in walker(header): + if start is not None and start.tag == 'begin': + fid = start.attrib['id'][1:] + fragments[fid] = {'content': [], 'themes': [], 'start_section': position, 'start_header': header.tag} + fragments[fid]['content'].append(start.tail) + elif start is not None and start.tag == 'motyw': + fid = start.attrib['id'][1:] + fragments[fid]['themes'].append(start.text) + fragments[fid]['content'].append(start.tail) + elif start is not None and start.tag == 'end': + fid = start.attrib['id'][1:] + if fid not in fragments: + continue # a broken node, skip it + frag = fragments[fid] + del fragments[fid] + + def jstr(l): + return u' '.join(map( + lambda x: x == None and u'(none)' or unicode(x), + l)) + + doc = add_part(snippets, + header_type=frag['start_header'], + header_index=frag['start_section'], + header_span=position - frag['start_section'] + 1, + fragment_anchor=fid, + content=u' '.join(filter(lambda s: s is not None, frag['content'])), + themes=frag['themes']) + + self.index.addDocument(doc) + elif start is not None: + for frag in fragments.values(): + frag['content'].append(start.text) + elif end is not None: + for frag in fragments.values(): + frag['content'].append(end.tail) + finally: + snippets.close() + def __enter__(self): self.open() @@ -390,34 +520,39 @@ class Search(IndexStore): class SearchResult(object): - def __init__(self, searcher, scoreDocs, score=None, highlight_query=None): + def __init__(self, searcher, scoreDocs, score=None, how_found=None, snippets=None): + self.snippets = [] + if score: self.score = score else: self.score = scoreDocs.score - self.fragments = [] - self.scores = {} - self.sections = [] + self.hits = [] stored = searcher.doc(scoreDocs.doc) self.book_id = int(stored.get("book_id")) + header_type = stored.get("header_type") + if not header_type: + return + + sec = (header_type, int(stored.get("header_index"))) + header_span = stored.get('header_span') + header_span = header_span is not None and int(header_span) or 1 + fragment = stored.get("fragment_anchor") - if fragment: - self.fragments.append(fragment) - self.scores[fragment] = scoreDocs.score - header_type = stored.get("header_type") - if header_type: - sec = (header_type, int(stored.get("header_index"))) - self.sections.append(sec) - self.scores[sec] = scoreDocs.score + hit = (sec + (header_span,), fragment, scoreDocs.score, {'how_found': how_found, 'snippets': snippets}) - self.snippets = [] + self.hits.append(hit) - def add_snippets(self, snippets): - self.snippets += snippets + def merge(self, other): + if self.book_id != other.book_id: + raise ValueError("this search result is or book %d; tried to merge with %d" % (self.book_id, other.book_id)) + self.hits += other.hits + if other.score > self.score: + self.score = other.score return self def get_book(self): @@ -425,27 +560,36 @@ class SearchResult(object): book = property(get_book) - def get_parts(self): - book = self.book - parts = [{"header": s[0], "position": s[1], '_score_key': s} for s in self.sections] \ - + [{"fragment": book.fragments.get(anchor=f), '_score_key':f} for f in self.fragments] + def process_hits(self): + frags = filter(lambda r: r[1] is not None, self.hits) + sect = filter(lambda r: r[1] is None, self.hits) + sect = filter(lambda s: 0 == len(filter( + lambda f: s[0][1] >= f[0][1] and s[0][1] < f[0][1] + f[0][2], + frags)), sect) - parts.sort(lambda a, b: cmp(self.scores[a['_score_key']], self.scores[b['_score_key']])) - print("bookid: %d parts: %s" % (self.book_id, parts)) - return parts + hits = [] - parts = property(get_parts) + for s in sect: + m = {'score': s[2], + 'header_index': s[0][1] + } + m.update(s[3]) + hits.append(m) - def merge(self, other): - if self.book_id != other.book_id: - raise ValueError("this search result is or book %d; tried to merge with %d" % (self.book_id, other.book_id)) - self.fragments += other.fragments - self.sections += other.sections - self.snippets += other.snippets - self.scores.update(other.scores) - if other.score > self.score: - self.score = other.score - return self + for f in frags: + frag = catalogue.models.Fragment.objects.get(anchor=f[1]) + m = {'score': f[2], + 'fragment': frag, + 'themes': frag.tags.filter(category='theme') + } + m.update(f[3]) + hits.append(m) + + hits.sort(lambda a, b: cmp(a['score'], b['score']), reverse=True) + + print("--- %s" % hits) + + return hits def __unicode__(self): return u'SearchResult(book_id=%d, score=%d)' % (self.book_id, self.score) @@ -466,6 +610,69 @@ class SearchResult(object): return cmp(self.score, other.score) +class Hint(object): + def __init__(self, search): + self.search = search + self.book_tags = {} + self.part_tags = [] + self._book = None + + def book(self, book): + self._book = book + + def tags(self, tags): + for t in tags: + if t.category in ['author', 'title', 'epoch', 'genre', 'kind']: + lst = self.book_tags.get(t.category, []) + lst.append(t) + self.book_tags[t.category] = lst + if t.category in ['theme']: + self.part_tags.append(t) + + def tag_filter(self, tags, field='tags'): + q = BooleanQuery() + + for tag in tags: + toks = self.search.get_tokens(tag.name, field=field) + tag_phrase = PhraseQuery() + for tok in toks: + tag_phrase.add(Term(field, tok)) + q.add(BooleanClause(tag_phrase, BooleanClause.Occur.MUST)) + + return QueryWrapperFilter(q) + + def book_filter(self): + tags = reduce(lambda a, b: a + b, self.book_tags.values(), []) + if tags: + return self.tag_filter(tags) + else: + return None + + def part_filter(self): + fs = [] + if self.part_tags: + fs.append(self.tag_filter(self.part_tags, field='themes')) + if self._book is not None: + fs.append(NumericRangeFilter.newIntRange('book_id', self._book.id, self._book.id, True, True)) + return MultiSearch.chain_filters(fs) + + def should_search_for_book(self): + return self._book is None + + def just_search_in(self, all): + """Holds logic to figure out which indexes should be search, when we have some hinst already""" + some = [] + for field in all: + if field == 'author' and 'author' in self.book_tags: + continue + if field == 'title' and self._book is not None: + continue + if (field == 'themes' or field == 'themes_pl') and self.part_tags: + continue + some.append(field) + return some + + class MultiSearch(Search): """Class capable of IMDb-like searching""" def get_tokens(self, searched, field='content'): @@ -535,68 +742,81 @@ class MultiSearch(Search): return BlockJoinQuery(query, self.parent_filter, BlockJoinQuery.ScoreMode.Total) - def search_perfect_book(self, searched, max_results=20, fuzzy=False): - qrys = [self.make_phrase(self.get_tokens(searched, field=fld), field=fld, fuzzy=fuzzy) for fld in ['author', 'title']] + def search_perfect_book(self, searched, max_results=20, fuzzy=False, hint=None): + fields_to_search = ['author', 'title'] + only_in = None + if hint: + if not hint.should_search_for_book(): + return [] + fields_to_search = hint.just_search_in(fields_to_search) + only_in = hint.book_filter() + + qrys = [self.make_phrase(self.get_tokens(searched, field=fld), field=fld, fuzzy=fuzzy) for fld in fields_to_search] books = [] for q in qrys: - top = self.searcher.search(q, max_results) + top = self.searcher.search(q, + self.chain_filters([only_in, self.term_filter(Term('is_book', 'true'))]), + max_results) for found in top.scoreDocs: books.append(SearchResult(self.searcher, found)) return books - def search_perfect_parts(self, searched, max_results=20, fuzzy=False): + def search_perfect_parts(self, searched, max_results=20, fuzzy=False, hint=None): qrys = [self.make_phrase(self.get_tokens(searched), field=fld, fuzzy=fuzzy) for fld in ['content']] + flt = None + if hint: + flt = hint.part_filter() + books = [] for q in qrys: - top = self.searcher.search(q, max_results) + top = self.searcher.search(q, + self.chain_filters([self.term_filter(Term('is_book', 'true'), inverse=True), + flt + ]), + max_results) for found in top.scoreDocs: - books.append(SearchResult(self.searcher, found).add_snippets(self.get_snippets(found, q))) + books.append(SearchResult(self.searcher, found, snippets=self.get_snippets(found, q))) return books - def search_everywhere(self, searched, max_results=20, fuzzy=False): + def search_everywhere(self, searched, max_results=20, fuzzy=False, hint=None): books = [] + only_in = None + + if hint: + only_in = hint.part_filter() # content only query : themes x content q = BooleanQuery() tokens = self.get_tokens(searched) - q.add(BooleanClause(self.make_term_query(tokens, field='themes', fuzzy=fuzzy), BooleanClause.Occur.MUST)) - q.add(BooleanClause(self.make_term_query(tokens, field='content', fuzzy=fuzzy), BooleanClause.Occur.SHOULD)) + if hint is None or hint.just_search_in(['themes_pl']) != []: + q.add(BooleanClause(self.make_term_query(tokens, field='themes_pl', + fuzzy=fuzzy), BooleanClause.Occur.MUST)) - topDocs = self.searcher.search(q, max_results) + q.add(BooleanClause(self.make_term_query(tokens, field='content', + fuzzy=fuzzy), BooleanClause.Occur.SHOULD)) + + topDocs = self.searcher.search(q, only_in, max_results) for found in topDocs.scoreDocs: books.append(SearchResult(self.searcher, found)) - # joined query themes/content x author/title/epochs/genres/kinds + # query themes/content x author/title/tags q = BooleanQuery() in_meta = BooleanQuery() in_content = BooleanQuery() - for fld in ['themes', 'content']: + for fld in ['themes', 'content', 'tags', 'author', 'title']: in_content.add(BooleanClause(self.make_term_query(tokens, field=fld, fuzzy=False), BooleanClause.Occur.SHOULD)) - in_meta.add(BooleanClause(self.make_term_query(self.get_tokens(searched, field='author'), field='author', fuzzy=fuzzy), BooleanClause.Occur.SHOULD)) - - for fld in ['title', 'epochs', 'genres', 'kinds']: - in_meta.add(BooleanClause(self.make_term_query(tokens, field=fld, fuzzy=fuzzy), BooleanClause.Occur.SHOULD)) - - q.add(BooleanClause(in_meta, BooleanClause.Occur.MUST)) - in_content_join = self.content_query(in_content) - q.add(BooleanClause(in_content_join, BooleanClause.Occur.MUST)) - # import pdb; pdb.set_trace() - collector = BlockJoinCollector(Sort.RELEVANCE, 100, True, True) - - self.searcher.search(q, collector) + topDocs = self.searcher.search(q, only_in, max_results) + for found in topDocs.scoreDocs: + books.append(SearchResult(self.searcher, found)) - top_groups = collector.getTopGroups(in_content_join, Sort.RELEVANCE, 0, max_results, 0, True) - if top_groups: - for grp in top_groups.groups: - for part in grp.scoreDocs: - books.append(SearchResult(self.searcher, part, score=grp.maxScore)) return books + def multisearch(self, query, max_results=50): """ @@ -640,9 +860,8 @@ class MultiSearch(Search): return None - - def do_search(self, query, max_results=50, collector=None): - tops = self.searcher.search(query, max_results) + def book_search(self, query, filter=None, max_results=50, collector=None): + tops = self.searcher.search(query, filter, max_results) #tops = self.searcher.search(p_content, max_results) bks = [] @@ -651,17 +870,117 @@ class MultiSearch(Search): b = catalogue.models.Book.objects.get(id=doc.get("book_id")) bks.append(b) print "%s (%d) -> %f" % (b, b.id, found.score) - return (bks, tops.totalHits) + return bks def get_snippets(self, scoreDoc, query, field='content'): htmlFormatter = SimpleHTMLFormatter() highlighter = Highlighter(htmlFormatter, QueryScorer(query)) stored = self.searcher.doc(scoreDoc.doc) - text = stored.get(field) + + # locate content. + snippets = Snippets(stored.get('book_id')).open() + try: + text = snippets.get((int(stored.get('snippets_position')), + int(stored.get('snippets_length')))) + finally: + snippets.close() + tokenStream = TokenSources.getAnyTokenStream(self.searcher.getIndexReader(), scoreDoc.doc, field, self.analyzer) # highlighter.getBestTextFragments(tokenStream, text, False, 10) + # import pdb; pdb.set_trace() snip = highlighter.getBestFragments(tokenStream, text, 3, "...") print('snips: %s' % snip) return [snip] + + @staticmethod + def enum_to_array(enum): + """ + Converts a lucene TermEnum to array of Terms, suitable for + addition to queries + """ + terms = [] + + while True: + t = enum.term() + if t: + terms.append(t) + if not enum.next(): break + + if terms: + return JArray('object')(terms, Term) + + def search_tags(self, query, filter=None, max_results=40): + tops = self.searcher.search(query, filter, max_results) + + tags = [] + for found in tops.scoreDocs: + doc = self.searcher.doc(found.doc) + tag = catalogue.models.Tag.objects.get(id=doc.get("tag_id")) + tags.append(tag) + print "%s (%d) -> %f" % (tag, tag.id, found.score) + + return tags + + def create_prefix_phrase(self, toks, field): + q = MultiPhraseQuery() + for i in range(len(toks)): + t = Term(field, toks[i]) + if i == len(toks) - 1: + pterms = MultiSearch.enum_to_array(PrefixTermEnum(self.searcher.getIndexReader(), t)) + if pterms: + q.add(pterms) + else: + q.add(t) + else: + q.add(t) + return q + + @staticmethod + def term_filter(term, inverse=False): + only_term = TermsFilter() + only_term.addTerm(term) + + if inverse: + neg = BooleanFilter() + neg.add(FilterClause(only_term, BooleanClause.Occur.MUST_NOT)) + only_term = neg + + return only_term + + def hint_tags(self, string, max_results=50): + toks = self.get_tokens(string, field='SIMPLE') + top = BooleanQuery() + + for field in ['tag_name', 'tag_name_pl']: + q = self.create_prefix_phrase(toks, field) + top.add(BooleanClause(q, BooleanClause.Occur.SHOULD)) + + no_book_cat = self.term_filter(Term("tag_category", "book"), inverse=True) + + return self.search_tags(top, no_book_cat, max_results=max_results) + + def hint_books(self, string, max_results=50): + toks = self.get_tokens(string, field='SIMPLE') + + q = self.create_prefix_phrase(toks, 'title') + + return self.book_search(q, self.term_filter(Term("is_book", "true")), max_results=max_results) + + @staticmethod + def chain_filters(filters, op=ChainedFilter.AND): + filters = filter(lambda x: x is not None, filters) + if not filters: + return None + chf = ChainedFilter(JArray('object')(filters, Filter), op) + return chf + + def filtered_categories(self, tags): + cats = {} + for t in tags: + cats[t.category] = True + return cats.keys() + + def hint(self): + return Hint(self) diff --git a/apps/search/management/__init__.py b/apps/search/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/search/management/commands/__init__.py b/apps/search/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/search/management/commands/checkindex.py b/apps/search/management/commands/checkindex.py new file mode 100644 index 000000000..b910277de --- /dev/null +++ b/apps/search/management/commands/checkindex.py @@ -0,0 +1,22 @@ + +from django.core.management.base import BaseCommand +from search import IndexChecker + +class Command(BaseCommand): + help = 'Check Lucene search index' + args = '' + + def handle(self, *args, **opts): + checker = IndexChecker() + status = checker.check() + if status.clean: + print "No problems found." + else: + if status.missingSegments: + print "Unable to locate." + print "Number of bad segments: %d / %d (max segment name is %d)" % \ + (status.numBadSegments, status.numSegments, status.maxSegmentName) + print "Total lost documents (due to bad segments) %d" % status.totLoseDocCount + if not status.validCounter: + print "Segment counter is not valid." + diff --git a/apps/search/management/commands/optimizeindex.py b/apps/search/management/commands/optimizeindex.py new file mode 100644 index 000000000..a8a4cf9dd --- /dev/null +++ b/apps/search/management/commands/optimizeindex.py @@ -0,0 +1,15 @@ + +from django.core.management.base import BaseCommand +from search import Index + +class Command(BaseCommand): + help = 'Optimize Lucene search index' + args = '' + + def handle(self, *args, **opts): + index = Index() + index.open() + try: + index.optimize() + finally: + index.close() diff --git a/apps/search/urls.py b/apps/search/urls.py index f93d65e79..607f094cd 100644 --- a/apps/search/urls.py +++ b/apps/search/urls.py @@ -5,6 +5,7 @@ from django.conf.urls.defaults import * urlpatterns = patterns('search.views', - url(r'^$', 'main', name='newsearch'), + url(r'^$', 'main', name='search'), + url(r'^hint/$', 'hint'), ) diff --git a/apps/search/views.py b/apps/search/views.py index d9b2f26d5..c50077005 100644 --- a/apps/search/views.py +++ b/apps/search/views.py @@ -1,14 +1,19 @@ +# -*- coding: utf-8 -*- from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib.auth.decorators import login_required from django.views.decorators import cache +from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect from catalogue.utils import get_random_hash -from catalogue.models import Book, Tag +from catalogue.models import Book, Tag, Fragment, TAG_CATEGORIES +from catalogue.fields import dumps +from catalogue.views import JSONResponse from catalogue import forms from search import MultiSearch, JVM, SearchResult from lucene import StringReader +from suggest.forms import PublishingSuggestForm import enchant @@ -17,7 +22,7 @@ dictionary = enchant.Dict('pl_PL') def did_you_mean(query, tokens): change = {} - + # sprawdzić, czy słowo nie jest aby autorem - proste szukanie termu w author! for t in tokens: print("%s ok? %s, sug: %s" %(t, dictionary.check(t), dictionary.suggest(t))) if not dictionary.check(t): @@ -31,10 +36,66 @@ def did_you_mean(query, tokens): for frm, to in change.items(): query = query.replace(frm, to) - + return query +def category_name(category): + try: + return filter(lambda c: c[0] == category, TAG_CATEGORIES)[0][1].encode('utf-8') + except IndexError: + raise KeyError("No category %s" % category) + + +def hint(request): + prefix = request.GET.get('term', '') + if len(prefix) < 2: + return JSONResponse(dumps(None)) + JVM.attachCurrentThread() + s = MultiSearch() + + hint = s.hint() + try: + tags = request.GET.get('tags', '') + hint.tags(Tag.get_tag_list(tags)) + except: + pass + + # tagi beda ograniczac tutaj + # ale tagi moga byc na ksiazce i na fragmentach + # jezeli tagi dot tylko ksiazki, to wazne zeby te nowe byly w tej samej ksiazce + # jesli zas dotycza themes, to wazne, zeby byly w tym samym fragmencie. + + tags = s.hint_tags(prefix) + books = s.hint_books(prefix) + + # TODO DODAC TU HINTY + + return JSONResponse( + [{'label': t.name, + 'category': category_name(t.category), + 'id': t.id, + 'url': t.get_absolute_url()} + for t in tags] + \ + [{'label': b.title, + 'category': category_name('book'), + 'id': b.id, + 'url': b.get_absolute_url()} + for b in books]) + + +def foo(s, q, tag_list=None): + hint = s.hint() + try: + tag_list = Tag.get_tag_list(tag_list) + hint.tags(tag_list) + except: + tag_list = None + + q = StringReader(q) + return (q, hint) + + def main(request): results = {} JVM.attachCurrentThread() # where to put this? @@ -43,24 +104,61 @@ def main(request): results = None query = None fuzzy = False + if 'q' in request.GET: + tags = request.GET.get('tags', '') query = request.GET['q'] + book_id = request.GET.get('book', None) + book = None + if book_id is not None: + book = get_object_or_404(Book, id=book_id) + + hint = srch.hint() + try: + tag_list = Tag.get_tag_list(tags) + except: + tag_list = [] + + if len(query) < 2: + return render_to_response('catalogue/search_too_short.html', {'tags': tag_list, 'prefix': query}, + context_instance=RequestContext(request)) + + hint.tags(tag_list) + hint.book(book) + toks = StringReader(query) fuzzy = 'fuzzy' in request.GET if fuzzy: fuzzy = 0.7 - - results = SearchResult.aggregate(srch.search_perfect_book(toks, fuzzy=fuzzy), - srch.search_perfect_parts(toks, fuzzy=fuzzy), - srch.search_everywhere(toks, fuzzy=fuzzy)) + results = SearchResult.aggregate(srch.search_perfect_book(toks, fuzzy=fuzzy, hint=hint), + srch.search_perfect_parts(toks, fuzzy=fuzzy, hint=hint), + srch.search_everywhere(toks, fuzzy=fuzzy, hint=hint)) results.sort(reverse=True) for r in results: - print r.parts + print r.hits + + if len(results) == 1: + if len(results[0].hits) == 0: + return HttpResponseRedirect(results[0].book.get_absolute_url()) + elif len(results[0].hits) == 1 and results[0].hits[0] is not None: + frag = Fragment.objects.get(anchor=results[0].hits[0]) + return HttpResponseRedirect(frag.get_absolute_url()) + elif len(results) == 0: + form = PublishingSuggestForm(initial={"books": query + ", "}) + return render_to_response('catalogue/search_no_hits.html', + {'tags': tag_list, 'prefix': query, "pubsuggest_form": form, + 'form': forms.SearchForm()}, + context_instance=RequestContext(request)) + + return render_to_response('catalogue/search_multiple_hits.html', + {'tags': tag_list, 'prefix': query, + 'results': results, 'from': forms.SearchForm()}, + context_instance=RequestContext(request)) - return render_to_response('newsearch/search.html', {'results': results, - 'did_you_mean': (query is not None) and - did_you_mean(query, srch.get_tokens(query, field='SIMPLE')), - 'fuzzy': fuzzy}, - context_instance=RequestContext(request)) + # return render_to_response('newsearch/search.html', {'results': results, + # 'did_you_mean': (query is not None) and + # did_you_mean(query, srch.get_tokens(query, field='SIMPLE')), + # 'fuzzy': fuzzy}, + # context_instance=RequestContext(request)) diff --git a/wolnelektury/settings.py b/wolnelektury/settings.py index ca0678455..814861436 100644 --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@ -164,7 +164,8 @@ CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True # CSS and JavaScript file groups COMPRESS_CSS = { 'all': { - 'source_filenames': ('css/master.css', 'css/jquery.autocomplete.css', 'css/jquery.countdown.css', 'css/master.plain.css', 'css/sponsors.css', 'css/facelist_2-0.css',), + 'source_filenames': ('css/master.css', 'css/jquery.countdown.css', + 'css/master.plain.css', 'css/sponsors.css', 'css/facelist_2-0.css', 'css/ui-lightness/jquery-ui-1.8.16.custom.css'), 'output_filename': 'css/all.min?.css', }, 'book': { @@ -179,11 +180,11 @@ COMPRESS_CSS = { COMPRESS_JS = { 'jquery': { - 'source_filenames': ('js/jquery.js',), + 'source_filenames': ('js/jquery.js', 'js/jquery-ui-1.8.16.custom.min.js'), 'output_filename': 'js/jquery.min.js', }, 'all': { - 'source_filenames': ('js/jquery.autocomplete.js', 'js/jquery.form.js', + 'source_filenames': ('js/jquery.form.js', 'js/jquery.countdown.js', 'js/jquery.countdown-pl.js', 'js/jquery.countdown-de.js', 'js/jquery.countdown-uk.js', 'js/jquery.countdown-es.js', 'js/jquery.countdown-lt.js', diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png new file mode 100644 index 000000000..954e22dbd Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png new file mode 100644 index 000000000..64ece5707 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png new file mode 100644 index 000000000..abdc01082 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png new file mode 100644 index 000000000..9b383f4d2 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png new file mode 100644 index 000000000..a23baad25 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 000000000..42ccba269 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png new file mode 100644 index 000000000..1b1972b56 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png new file mode 100644 index 000000000..f1273672d Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png b/wolnelektury/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png new file mode 100644 index 000000000..359397acf Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-icons_222222_256x240.png b/wolnelektury/static/css/ui-lightness/images/ui-icons_222222_256x240.png new file mode 100644 index 000000000..b273ff111 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-icons_222222_256x240.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png b/wolnelektury/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png new file mode 100644 index 000000000..a641a371a Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png b/wolnelektury/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png new file mode 100644 index 000000000..85e63e9f6 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png b/wolnelektury/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png new file mode 100644 index 000000000..e117effa3 Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png differ diff --git a/wolnelektury/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png b/wolnelektury/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png new file mode 100644 index 000000000..42f8f992c Binary files /dev/null and b/wolnelektury/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png differ diff --git a/wolnelektury/static/css/ui-lightness/jquery-ui-1.8.16.custom.css b/wolnelektury/static/css/ui-lightness/jquery-ui-1.8.16.custom.css new file mode 100644 index 000000000..da10fff71 --- /dev/null +++ b/wolnelektury/static/css/ui-lightness/jquery-ui-1.8.16.custom.css @@ -0,0 +1,342 @@ +/* + * jQuery UI CSS Framework 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { display: none; } +.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } +.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } +.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } +.ui-helper-clearfix { display: inline-block; } +/* required comment for clearfix to work in Opera \*/ +* html .ui-helper-clearfix { height:1%; } +.ui-helper-clearfix { display:block; } +/* end clearfix */ +.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { cursor: default !important; } + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } + + +/* + * jQuery UI CSS Framework 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Tahoma,%20Verdana,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1.1em; } +.ui-widget .ui-widget { font-size: 1em; } +.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1em; } +.ui-widget-content { border: 1px solid #dddddd; background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x; color: #333333; } +.ui-widget-content a { color: #333333; } +.ui-widget-header { border: 1px solid #e78f08; background: #f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; } +.ui-widget-header a { color: #ffffff; } + +/* Interaction states +----------------------------------*/ +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; background: #f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #1c94c4; } +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #1c94c4; text-decoration: none; } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #fbcb09; background: #fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #c77405; } +.ui-state-hover a, .ui-state-hover a:hover { color: #c77405; text-decoration: none; } +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #fbd850; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #eb8f00; } +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #eb8f00; text-decoration: none; } +.ui-widget :active { outline: none; } + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fed22f; background: #ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x; color: #363636; } +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat; color: #ffffff; } +.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; } +.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; } +.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } +.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } +.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-header .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } +.ui-state-default .ui-icon { background-image: url(images/ui-icons_ef8c08_256x240.png); } +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); } +.ui-state-active .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); } +.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_228ef1_256x240.png); } +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffd27a_256x240.png); } + +/* positioning */ +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-off { background-position: -96px -144px; } +.ui-icon-radio-on { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; } + +/* Overlays */ +.ui-widget-overlay { background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat; opacity: .50;filter:Alpha(Opacity=50); } +.ui-widget-shadow { margin: -5px 0 0 -5px; padding: 5px; background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 5px; -khtml-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }/* + * jQuery UI Autocomplete 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Autocomplete#theming + */ +.ui-autocomplete { position: absolute; cursor: default; } + +/* workarounds */ +* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ + +/* + * jQuery UI Menu 1.8.16 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Menu#theming + */ +.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.ui-menu .ui-menu { + margin-top: -3px; +} +.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.ui-menu .ui-menu-item a.ui-state-hover, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} diff --git a/wolnelektury/static/js/catalogue.js b/wolnelektury/static/js/catalogue.js index ee8a045dc..02f5b6c5c 100644 --- a/wolnelektury/static/js/catalogue.js +++ b/wolnelektury/static/js/catalogue.js @@ -90,9 +90,20 @@ function changeBannerText() { } } -function autocomplete_result_handler(event, item) { - $(event.target).closest('form').submit(); +function autocomplete_format_item(ul, item) { + return $("
  • ").data('item.autocomplete', item) + .append(''+item.label+ ' ('+item.category+')') + .appendTo(ul); } + +function autocomplete_result_handler(event, ui) { + if (ui.item.url != undefined) { + location.href = ui.item.url; + } else { + $(event.target).closest('form').submit(); + } +} + function serverTime() { var time = null; $.ajax({url: '/katalog/zegar/', diff --git a/wolnelektury/static/js/jquery-ui-1.8.16.custom.min.js b/wolnelektury/static/js/jquery-ui-1.8.16.custom.min.js new file mode 100644 index 000000000..a9c6fa316 --- /dev/null +++ b/wolnelektury/static/js/jquery-ui-1.8.16.custom.min.js @@ -0,0 +1,149 @@ +/*! + * jQuery UI 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI + */ +(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.16", +keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({propAttr:c.fn.prop||c.fn.attr,_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d= +this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this, +"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart": +"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight, +outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a, +"tabindex"),d=isNaN(b);return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&& +a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted= +false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); +;/* + * jQuery UI Position 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Position + */ +(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.setTimeout){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j={top:b.of.pageY, +left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/2;if(b.at[1]==="bottom")j.top+= +k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+(parseInt(c.curCSS(this,"marginRight",true))||0),w=m+q+(parseInt(c.curCSS(this,"marginBottom",true))||0),i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]==="center")i.top-= +m/2;i.left=Math.round(i.left);i.top=Math.round(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();b.left= +d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];b.left+= +a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=c(b), +g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery); +;/* + * jQuery UI Draggable 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Draggables + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== +"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= +this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;if(b.iframeFix)d(b.iframeFix===true?"iframe":b.iframeFix).each(function(){d('
    ').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")});return true},_mouseStart:function(a){var b=this.options; +this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}); +this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);d.ui.ddmanager&&d.ui.ddmanager.dragStart(this,a);return true}, +_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b= +false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration, +10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},_mouseUp:function(a){this.options.iframeFix===true&&d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)});d.ui.ddmanager&&d.ui.ddmanager.dragStop(this,a);return d.ui.mouse.prototype._mouseUp.call(this,a)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle|| +!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone().removeAttr("id"):this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&& +a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent= +this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"), +10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"), +10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[a.containment=="document"?0:d(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,a.containment=="document"?0:d(window).scrollTop()-this.offset.relative.top-this.offset.parent.top, +(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"?0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){a=d(a.containment);var b=a[0];if(b){a.offset();var c=d(b).css("overflow")!= +"hidden";this.containment=[(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0),(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0),(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"), +10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom];this.relative_container=a}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+ +this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&& +!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,h=a.pageY;if(this.originalPosition){var g;if(this.containment){if(this.relative_container){g=this.relative_container.offset();g=[this.containment[0]+g.left,this.containment[1]+g.top,this.containment[2]+g.left,this.containment[3]+g.top]}else g=this.containment;if(a.pageX-this.offset.click.leftg[2])e=g[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>g[3])h=g[3]+this.offset.click.top}if(b.grid){h=b.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/b.grid[1])*b.grid[1]:this.originalPageY;h=g?!(h-this.offset.click.topg[3])?h:!(h-this.offset.click.topg[2])?e:!(e-this.offset.click.left=0;i--){var j=c.snapElements[i].left,l=j+c.snapElements[i].width,k=c.snapElements[i].top,m=k+c.snapElements[i].height;if(j-e").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&& +a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"); +d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&& +b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source= +this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(d("").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, +"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery); +(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", +-1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id"); +this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b, +this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| +this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| +this.first()?":last":":first"))},hasScroll:function(){return this.element.height(){% title_from_tags tags %} - {% breadcrumbs tags %} +
    +

    {{ form.q }} {% trans "or" %} {% trans "return to main page" %}

    +
    +
    +
      + {% for result in results %} +
    1. +

      {{result.book.pretty_title}} (id: {{result.book_id}}, score: {{result.score}})

      +
        + {% for hit in result.process_hits %} +
      • + {% if hit.fragment %} +
        Tagi/Motywy: {% for tag in hit.themes %}{{tag.name}} {% endfor %}
        + {% endif %} + {% for snip in hit.snippets %} + {{snip|safe}}
        + {% endfor %} +
      • + {% endfor %} + +
      +
    2. + {% empty %} +

      No results.

      + {% endfor %} +
    +
    + + +{% comment %}

    {% trans "More than one result matching the criteria found." %}

      @@ -24,6 +52,7 @@ {% endfor %}
    +{% endcomment %}
    @@ -31,4 +60,4 @@

    * {% trans "Loading" %}

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/wolnelektury/templates/newsearch/search.html b/wolnelektury/templates/newsearch/search.html index af36b5988..c494ca602 100644 --- a/wolnelektury/templates/newsearch/search.html +++ b/wolnelektury/templates/newsearch/search.html @@ -10,7 +10,7 @@ {% block body %}

    Search

    -
    +

    @@ -27,10 +27,14 @@

      {% for result in results %}
    1. -

      {{result.book.pretty_title}} (id: {{result.book.id}}, score: {{result.score}})

      +

      {{result.book.pretty_title}} (id: {{result.book_id}}, score: {{result.score}})

        - {% for snippet in result.snippets %} -
      • {{snippet|safe}}
      • + {% for hit in result.hits %} +
      • + {% for snip in hit.3.snippets %} + {{snip|safe}}
        + {% endfor %} +
      • {% endfor %} {% for part in result.parts %}