1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 from django.conf import settings
6 from .models import Tag, Book
7 from os.path import getmtime
9 from collections import defaultdict
12 BOOK_CATEGORIES = ('author', 'epoch', 'genre', 'kind')
18 def get_top_level_related_tags(tags, categories=None):
20 Finds tags related to given tags through books, and counts their usage.
22 Takes ancestry into account: if a tag is applied to a book, its
23 usage on the book's descendants is ignored.
25 global _COUNTERS, _COUNTER_TIME
26 # First, check that we have a valid and recent version of the counters.
27 if getmtime(settings.CATALOGUE_COUNTERS_FILE) > _COUNTER_TIME:
28 with open(settings.CATALOGUE_COUNTERS_FILE) as f:
29 _COUNTERS = cPickle.load(f)
31 tagids = tuple(sorted(t.pk for t in tags))
33 related_ids = _COUNTERS['next'][tagids]
37 related = Tag.objects.filter(pk__in=related_ids)
39 # TODO: do we really need that?
40 if categories is not None:
41 related = related.filter(category__in=categories)
44 tag.count = _COUNTERS['count'][tuple(sorted(tagids + (tag.pk,)))]
48 def update_counters():
49 def combinations(things):
51 for c in combinations(things[1:]):
53 yield (things[0],) + c
57 def count_for_book(book, count_by_combination=None, parent_combinations=None):
58 if not parent_combinations:
59 parent_combinations = set()
60 tags = sorted(tuple(t.pk for t in book.tags.filter(category__in=('author', 'genre', 'epoch', 'kind'))))
61 combs = list(combinations(tags))
63 if c not in parent_combinations:
64 count_by_combination[c] += 1
65 combs_for_child = set(list(parent_combinations) + combs)
66 for child in book.children.all():
67 count_for_book(child, count_by_combination, combs_for_child)
69 count_by_combination = defaultdict(lambda: 0)
70 for b in Book.objects.filter(parent=None):
71 count_for_book(b, count_by_combination)
73 next_combinations = defaultdict(set)
74 # Now build an index of all combinations.
75 for c in count_by_combination.keys():
79 rest = tuple(x for x in c if x != n)
80 next_combinations[rest].add(n)
83 "count": dict(count_by_combination),
84 "next": dict(next_combinations),
87 with open(settings.CATALOGUE_COUNTERS_FILE, 'w') as f:
88 cPickle.dump(counters, f)