Nicer search, minor fixes.
[wolnelektury.git] / src / search / forms.py
1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 from django.apps import apps
5 from django.conf import settings
6 from django.contrib.postgres.search import SearchHeadline, SearchQuery
7 from django import forms
8 from django.utils.translation import gettext_lazy as _
9 from catalogue.constants import LANGUAGES_3TO2
10 import catalogue.models
11 import pdcounter.models
12 import picture.models
13 from .fields import JQueryAutoCompleteSearchField, InlineRadioWidget
14 from .utils import UnaccentSearchQuery, UnaccentSearchVector
15
16
17 class SearchForm(forms.Form):
18     q = JQueryAutoCompleteSearchField(label=_('Search'))
19     # {'minChars': 2, 'selectFirst': True, 'cacheLength': 50, 'matchContains': "word"})
20
21     def __init__(self, source, *args, **kwargs):
22         kwargs['auto_id'] = False
23         super(SearchForm, self).__init__(*args, **kwargs)
24         self.fields['q'].widget.attrs['id'] = 'search'
25         self.fields['q'].widget.attrs['autocomplete'] = 'off'
26         self.fields['q'].widget.attrs['data-source'] = source
27         if 'q' not in self.data:
28             self.fields['q'].widget.attrs['placeholder'] = _('title, author, epoch, kind, genre, phrase')
29
30
31 class SearchFilters(forms.Form):
32     q = forms.CharField(
33         required=False, widget=forms.HiddenInput(),
34         min_length=2, max_length=256,
35     )
36     format = forms.ChoiceField(required=False, choices=[
37         ('', 'wszystkie'),
38         ('text', 'tekst'),
39         ('audio', 'audiobook'),
40         ('daisy', 'Daisy'),
41         ('art', 'obraz'),
42         #('theme', 'motywy'),
43     ], widget=InlineRadioWidget())
44     lang = forms.ChoiceField(required=False)
45     epoch = forms.ChoiceField(required=False)
46     genre = forms.ChoiceField(required=False)
47     category = forms.ChoiceField(required=False, choices=[
48         ('', 'wszystkie'),
49         ('author', 'autor'),
50         #('translator', 'tłumacz'),
51         ('theme', 'motyw'),
52         ('genre', 'gatunek'),
53         ('book', 'tytuł'),
54         ('art', 'obraz'),
55         ('collection', 'kolekcja'),
56         ('quote', 'cytat'),
57     ], widget=InlineRadioWidget())
58
59     def __init__(self, *args, **kwargs):
60         super().__init__(*args, **kwargs)
61
62         langs = dict(settings.LANGUAGES)
63         self.fields['lang'].choices = [('', 'wszystkie')] + [
64             (
65                 b,
66                 langs.get(LANGUAGES_3TO2.get(b, b), b)
67             )
68             for b in catalogue.models.Book.objects.values_list(
69                     'language', flat=True
70             ).distinct().order_by()
71         ]
72         self.fields['epoch'].choices = [('', 'wszystkie')] + [
73             (b.slug, b.name)
74             for b in catalogue.models.Tag.objects.filter(category='epoch')
75         ]
76         self.fields['genre'].choices = [('', 'wszystkie')] + [
77             (b.slug, b.name)
78             for b in catalogue.models.Tag.objects.filter(category='genre')
79         ]
80
81     def get_querysets(self):
82         qs = {
83             'author': catalogue.models.Tag.objects.filter(category='author'),
84             'pdauthor': pdcounter.models.Author.objects.all(),
85             'theme': catalogue.models.Tag.objects.filter(category='theme'),
86             'genre': catalogue.models.Tag.objects.filter(category='genre'),
87             'collection': catalogue.models.Collection.objects.all(),
88             'book': catalogue.models.Book.objects.filter(findable=True),
89             'pdbook': pdcounter.models.BookStub.objects.all(),
90             'snippet': catalogue.models.Snippet.objects.filter(book__findable=True),
91             'art': picture.models.Picture.objects.all(),
92             # art pieces
93         }
94         if self.cleaned_data['category']:
95             c = self.cleaned_data['category']
96             if c != 'author':
97                 qs['author'] = qs['author'].none()
98                 qs['pdauthor'] = qs['pdauthor'].none()
99             if c != 'theme': qs['theme'] = qs['theme'].none()
100             if c != 'genre': qs['genre'] = qs['genre'].none()
101             if c != 'collection': qs['collection'] = qs['collection'].none()
102             if c != 'book':
103                 qs['book'] = qs['book'].none()
104                 qs['pdbook'] = qs['pdbook'].none()
105             if c != 'quote': qs['snippet'] = qs['snippet'].none()
106             if c != 'art': qs['art'] = qs['art'].none()
107             qs['art'] = picture.models.Picture.objects.none()
108
109         if self.cleaned_data['format']:
110             c = self.cleaned_data['format']
111             qs['author'] = qs['author'].none()
112             qs['pdauthor'] = qs['pdauthor'].none()
113             qs['theme'] = qs['theme'].none()
114             qs['genre'] = qs['genre'].none()
115             qs['collection'] = qs['collection'].none()
116             if c == 'art':
117                 qs['book'] = qs['book'].none()
118                 qs['pdbook'] = qs['pdbook'].none()
119                 qs['snippet'] = qs['snippet'].none()
120             if c in ('text', 'audio', 'daisy'):
121                 qs['art'] = qs['art'].none()
122                 if c == 'audio':
123                     qs['book'] = qs['book'].filter(media__type='mp3')
124                     qs['pdbook'] = qs['book'].none()
125                     qs['snippet'] = qs['snippet'].filter(book__media__type='mp3')
126                 elif c == 'daisy':
127                     qs['book'] = qs['book'].filter(media__type='daisy')
128                     qs['snippet'] = qs['snippet'].filter(book__media__type='daisy')
129
130         if self.cleaned_data['lang']:
131             qs['author'] = qs['author'].none()
132             qs['pdauthor'] = qs['pdauthor'].none()
133             qs['theme'] = qs['theme'].none()
134             qs['genre'] = qs['genre'].none()
135             qs['art'] = qs['art'].none()
136             qs['collection'] = qs['collection'].none()
137             qs['book'] = qs['book'].filter(language=self.cleaned_data['lang'])
138             qs['pdbook'] = qs['pdbook'].none()
139             qs['snippet'] = qs['snippet'].filter(book__language=self.cleaned_data['lang'])
140
141         for tag_cat in ('epoch', 'genre'):
142             c = self.cleaned_data[tag_cat]
143             if c:
144                 # FIXME nonexistent
145                 t = catalogue.models.Tag.objects.get(category=tag_cat, slug=c)
146                 qs['author'] = qs['author'].none()
147                 qs['pdauthor'] = qs['pdauthor'].none()
148                 qs['theme'] = qs['theme'].none()
149                 qs['genre'] = qs['genre'].none()
150                 qs['collection'] = qs['collection'].none()
151                 qs['book'] = qs['book'].filter(tag_relations__tag=t)
152                 qs['pdbook'] = qs['pdbook'].none()
153                 qs['snippet'] = qs['snippet'].filter(book__tag_relations__tag=t)
154                 qs['art'] = qs['art'].filter(tag_relations__tag=t)
155             
156         return qs
157
158     def results(self):
159         qs = self.get_querysets()
160         query = self.cleaned_data['q']
161         squery = UnaccentSearchQuery(query, config=settings.SEARCH_CONFIG)
162         query = SearchQuery(query, config=settings.SEARCH_CONFIG)
163         books = qs['book'].annotate(
164             search_vector=UnaccentSearchVector('title')
165         ).filter(search_vector=squery)
166         books = books.exclude(ancestor__in=books).order_by('-popularity__count')
167
168         snippets = qs['snippet'].filter(search_vector=squery).annotate(
169                     headline=SearchHeadline(
170                         'text',
171                         query,
172                         config=settings.SEARCH_CONFIG,
173                         start_sel='<strong>',
174                         stop_sel='</strong>',
175                     )
176                 ).order_by('-book__popularity__count', 'sec')[:100]
177         snippets_by_book = {}
178         for snippet in snippets:
179             snippet_list = snippets_by_book.setdefault(snippet.book, [])
180             if len(snippet_list) < 3:
181                 snippet_list.append(snippet)
182
183         return {
184             'author': qs['author'].annotate(
185                 search_vector=UnaccentSearchVector('name_pl')
186             ).filter(search_vector=squery),
187             'theme': qs['theme'].annotate(
188                 search_vector=UnaccentSearchVector('name_pl')
189             ).filter(search_vector=squery),
190             'genre': qs['genre'].annotate(
191                 search_vector=UnaccentSearchVector('name_pl')
192             ).filter(search_vector=squery),
193             'collection': qs['collection'].annotate(
194                 search_vector=UnaccentSearchVector('title')
195             ).filter(search_vector=squery),
196             'book': books[:100],
197             'art': qs['art'].annotate(
198                 search_vector=UnaccentSearchVector('title')
199             ).filter(search_vector=squery)[:100],
200             'snippet': snippets_by_book,
201             'pdauthor': pdcounter.models.Author.search(squery, qs=qs['pdauthor']),
202             'pdbook': pdcounter.models.BookStub.search(squery, qs=qs['pdbook']),
203         }
204