Add Book.ancestor m2m.
[wolnelektury.git] / apps / catalogue / helpers.py
1 from django.db import connection
2 from django.contrib.contenttypes.models import ContentType
3 from django.utils.translation import get_language
4 from picture.models import Picture, PictureArea
5 from catalogue.models import Fragment, Tag, Book
6
7
8 def _get_tag_relations_sql(tags):
9     select = """
10         SELECT Rx.object_id, Rx.content_type_id
11         FROM catalogue_tag_relation Rx"""
12     joins = []
13     where = ['WHERE Rx.tag_id = %d' % tags[0].pk]
14     for i, tag in enumerate(tags[1:]):
15         joins.append('INNER JOIN catalogue_tag_relation TR%(i)d '
16             'ON TR%(i)d.object_id = Rx.object_id '
17             'AND TR%(i)d.content_type_id = Rx.content_type_id' % {'i': i})
18         where.append('AND TR%d.tag_id = %d' % (i, tag.pk))
19     return " ".join([select] + joins + where)
20
21
22
23 def get_related_tags(tags):
24     # Get Tag fields for constructing tags in a raw query.
25     tag_fields = ('id', 'category', 'slug', 'sort_key', 'name_%s' % get_language())
26     tag_fields = ', '.join(
27             'T.%s' % connection.ops.quote_name(field)
28         for field in tag_fields)
29     tag_ids = tuple(t.pk for t in tags)
30
31     # This is based on fragments/areas sharing their works tags
32     qs = Tag.objects.raw('''
33         SELECT ''' + tag_fields + ''', COUNT(T.id) count
34         FROM (
35             -- R: TagRelations of all objects tagged with the given tags.
36             WITH R AS (
37                 ''' + _get_tag_relations_sql(tags) + '''
38             )
39
40             SELECT ''' + tag_fields + ''', MAX(R4.object_id) ancestor
41
42             FROM R R1
43
44             -- R2: All tags of the found objects.
45             JOIN catalogue_tag_relation R2
46                 ON R2.object_id = R1.object_id
47                     AND R2.content_type_id = R1.content_type_id
48
49             -- Tag data for output.
50             JOIN catalogue_tag T
51                 ON T.id=R2.tag_id
52
53             -- Special case for books:
54             -- We want to exclude from output all the relations
55             -- between a book and a tag, if there's a relation between
56             -- the the book's ancestor and the tag in the result.
57             LEFT JOIN catalogue_book_ancestor A
58                 ON A.from_book_id = R1.object_id
59                     AND R1.content_type_id = %s
60             LEFT JOIN catalogue_tag_relation R3
61                 ON R3.tag_id = R2.tag_id
62                     AND R3.content_type_id = R1.content_type_id
63                     AND R3.object_id = A.to_book_id
64             LEFT JOIN R R4
65                 ON R4.object_id = R3.object_id
66                 AND R4.content_type_id = R3.content_type_id
67
68             WHERE
69                 -- Exclude from the result the tags we started with.
70                 R2.tag_id NOT IN %s
71                 -- Special case for books: exclude descendants.
72                 -- AND R4.object_id IS NULL
73                 AND (
74                     -- Only count fragment tags on fragments
75                     -- and book tags for books.
76                     (R2.content_type_id IN %s AND T.category IN %s)
77                     OR
78                     (R2.content_type_id IN %s AND T.category IN %s)
79                 )
80
81             GROUP BY T.id, R2.object_id, R2.content_type_id
82
83         ) T
84         -- Now group by tag and count occurencies.
85         WHERE ancestor IS NULL
86         GROUP BY ''' + tag_fields + '''
87         ORDER BY T.sort_key
88         ''', params=(
89             ContentType.objects.get_for_model(Book).pk,
90             tag_ids,
91             tuple(ContentType.objects.get_for_model(model).pk
92                 for model in (Fragment, PictureArea)),
93             ('theme', 'object'),
94             tuple(ContentType.objects.get_for_model(model).pk
95                 for model in (Book, Picture)),
96             ('author', 'epoch', 'genre', 'kind'),
97         ))
98     return qs
99
100
101 def get_fragment_related_tags(tags):
102     tag_fields = ', '.join(
103         'T.%s' % (connection.ops.quote_name(field.column))
104         for field in Tag._meta.fields)
105
106     tag_ids = tuple(t.pk for t in tags)
107         # This is based on fragments/areas sharing their works tags
108     return Tag.objects.raw('''
109         SELECT T.*, COUNT(T.id) count
110         FROM (
111
112             SELECT T.*
113
114             -- R1: TagRelations of all objects tagged with the given tags.
115             FROM (
116                 ''' + _get_tag_relations_sql(tags) + '''
117             ) R1
118
119             -- R2: All tags of the found objects.
120             JOIN catalogue_tag_relation R2
121                 ON R2.object_id = R1.object_id
122                     AND R2.content_type_id = R1.content_type_id
123
124             -- Tag data for output.
125             JOIN catalogue_tag T
126                 ON T.id = R2.tag_id
127
128             WHERE
129                 -- Exclude from the result the tags we started with.
130                 R2.tag_id NOT IN %s
131             GROUP BY T.id, R2.object_id, R2.content_type_id
132
133         ) T
134         -- Now group by tag and count occurencies.
135         GROUP BY ''' + tag_fields + '''
136         ORDER BY T.sort_key
137         ''', params=(
138             tag_ids,
139         ))
140
141
142 def tags_usage_for_books(categories):
143     tag_fields = ', '.join(
144             'T.%s' % (connection.ops.quote_name(field.column))
145         for field in Tag._meta.fields)
146
147     # This is based on fragments/areas sharing their works tags
148     return Tag.objects.raw('''
149         SELECT T.*, COUNT(T.id) count
150         FROM (
151             SELECT T.*
152
153             FROM catalogue_tag_relation R1
154
155             -- Tag data for output.
156             JOIN catalogue_tag T
157                 ON T.id=R1.tag_id
158
159             -- We want to exclude from output all the relations
160             -- between a book and a tag, if there's a relation between
161             -- the the book's ancestor and the tag in the result.
162             LEFT JOIN catalogue_book_ancestor A
163                 ON A.from_book_id=R1.object_id
164             LEFT JOIN catalogue_tag_relation R3
165                 ON R3.tag_id = R1.tag_id
166                     AND R3.content_type_id = R1.content_type_id
167                     AND R3.object_id = A.to_book_id
168
169             WHERE
170                 R1.content_type_id = %s
171                 -- Special case for books: exclude descendants.
172                 AND R3.object_id IS NULL
173                 AND T.category IN %s
174
175             -- TODO:
176             -- Shouldn't it just be 'distinct'?
177             -- Maybe it's faster this way.
178             GROUP BY T.id, R1.object_id, R1.content_type_id
179
180         ) T
181         -- Now group by tag and count occurencies.
182         GROUP BY ''' + tag_fields + '''
183         ORDER BY T.sort_key
184         ''', params=(
185             ContentType.objects.get_for_model(Book).pk,
186             tuple(categories),
187         ))
188
189
190 def tags_usage_for_works(categories):
191     tag_fields = ', '.join(
192             'T.%s' % (connection.ops.quote_name(field.column))
193         for field in Tag._meta.fields)
194
195     return Tag.objects.raw('''
196         SELECT T.*, COUNT(T.id) count
197         FROM (
198
199             SELECT T.*
200
201             FROM catalogue_tag_relation R1
202
203             -- Tag data for output.
204             JOIN catalogue_tag T
205                 ON T.id = R1.tag_id
206
207             -- Special case for books:
208             -- We want to exclude from output all the relations
209             -- between a book and a tag, if there's a relation between
210             -- the the book's ancestor and the tag in the result.
211             LEFT JOIN catalogue_book_ancestor A
212                 ON A.from_book_id = R1.object_id
213                     AND R1.content_type_id = %s
214             LEFT JOIN catalogue_tag_relation R3
215                 ON R3.tag_id = R1.tag_id
216                     AND R3.content_type_id = R1.content_type_id
217                     AND R3.object_id = A.to_book_id
218
219             WHERE
220                 R1.content_type_id IN %s
221                 -- Special case for books: exclude descendants.
222                 AND R3.object_id IS NULL
223                 AND T.category IN %s
224
225             -- TODO:
226             -- Shouldn't it just be 'distinct'?
227             -- Maybe it's faster this way.
228             GROUP BY T.id, R1.object_id, R1.content_type_id
229
230         ) T
231         -- Now group by tag and count occurencies.
232         GROUP BY ''' + tag_fields + '''
233         ORDER BY T.sort_key
234        
235         ''', params=(
236             ContentType.objects.get_for_model(Book).pk,
237             tuple(ContentType.objects.get_for_model(model).pk for model in (Book, Picture)),
238             categories,
239         ))
240
241
242 def tags_usage_for_fragments(categories):
243     return Tag.objects.raw('''
244         SELECT t.*, count(t.id)
245         from catalogue_tag_relation r
246         join catalogue_tag t
247             on t.id = r.tag_id
248         where t.category IN %s
249         group by t.id
250         order by t.sort_key
251         ''', params=(
252             categories,
253         ))