db optimizations
[wolnelektury.git] / src / api / management / commands / mobileinit.py
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.
4 #
5 from datetime import datetime
6 import os.path
7 import sqlite3
8 from django.core.management.base import BaseCommand
9
10 from api.helpers import timestamp
11 from api.settings import MOBILE_INIT_DB
12 from catalogue.models import Book, Tag
13 from wolnelektury.utils import makedirs
14
15
16 class Command(BaseCommand):
17     help = 'Creates an initial SQLite file for the mobile app.'
18
19     def handle(self, **options):
20         # those should be versioned
21         last_checked = timestamp(datetime.now())
22         db = init_db(last_checked)
23         for b in Book.objects.all():
24             add_book(db, b)
25         for t in Tag.objects.exclude(
26                 category__in=('book', 'set', 'theme')).exclude(items=None):
27             # only add non-empty tags
28             add_tag(db, t)
29         db.commit()
30         db.close()
31         current(last_checked)
32
33
34 def pretty_size(size):
35     """ Turns size in bytes into a prettier string.
36
37         >>> pretty_size(100000)
38         '97 KiB'
39     """
40     if not size:
41         return None
42     units = ['B', 'KiB', 'MiB', 'GiB']
43     size = float(size)
44     unit = units.pop(0)
45     while size > 1000 and units:
46         size /= 1024
47         unit = units.pop(0)
48     if size < 10:
49         return "%.1f %s" % (size, unit)
50     return "%d %s" % (size, unit)
51     #
52     # if not isinstance(value, unicode):
53     #     value = unicode(value, 'utf-8')
54     #
55     # # try to replace chars
56     # value = re.sub('[^a-zA-Z0-9\\s\\-]{1}', replace_char, value)
57     # value = value.lower()
58     # value = re.sub(r'[^a-z0-9{|}]+', '~', value)
59     #
60     # return value.encode('ascii', 'ignore')
61
62
63 def init_db(last_checked):
64     makedirs(MOBILE_INIT_DB)
65     db = sqlite3.connect(os.path.join(MOBILE_INIT_DB, 'initial.db-%d' % last_checked))
66
67     schema = """
68 CREATE TABLE book (
69     id INTEGER PRIMARY KEY, 
70     title VARCHAR,
71     cover VARCHAR,
72     html_file VARCHAR, 
73     html_file_size INTEGER, 
74     parent INTEGER,
75     parent_number INTEGER,
76
77     sort_key VARCHAR,
78     pretty_size VARCHAR,
79     authors VARCHAR,
80     _local BOOLEAN
81 );
82 CREATE INDEX IF NOT EXISTS book_title_index ON book (sort_key);
83 CREATE INDEX IF NOT EXISTS book_title_index ON book (title);
84 CREATE INDEX IF NOT EXISTS book_parent_index ON book (parent);
85
86 CREATE TABLE tag (
87     id INTEGER PRIMARY KEY, 
88     name VARCHAR, 
89     category VARCHAR, 
90     sort_key VARCHAR, 
91     books VARCHAR);
92 CREATE INDEX IF NOT EXISTS tag_name_index ON tag (name);
93 CREATE INDEX IF NOT EXISTS tag_category_index ON tag (category);
94 CREATE INDEX IF NOT EXISTS tag_sort_key_index ON tag (sort_key);
95
96 CREATE TABLE state (last_checked INTEGER);
97 """
98
99     db.executescript(schema)
100     db.execute("INSERT INTO state VALUES (:last_checked)", locals())
101     return db
102
103
104 def current(last_checked):
105     target = os.path.join(MOBILE_INIT_DB, 'initial.db')
106     if os.path.lexists(target):
107         os.unlink(target)
108     os.symlink(
109         'initial.db-%d' % last_checked,
110         target,
111     )
112
113
114 book_sql = """
115     INSERT INTO book 
116         (id, title, cover, html_file,  html_file_size, parent, parent_number, sort_key, pretty_size, authors) 
117     VALUES 
118         (:id, :title, :cover, :html_file, :html_file_size, :parent, :parent_number, :sort_key, :size_str, :authors);
119 """
120 book_tag_sql = "INSERT INTO book_tag (book, tag) VALUES (:book, :tag);"
121 tag_sql = """
122     INSERT INTO tag
123         (id, category, name, sort_key, books)
124     VALUES
125         (:id, :category, :name, :sort_key, :book_ids);
126 """
127 categories = {'author': 'autor',
128               'epoch': 'epoka',
129               'genre': 'gatunek',
130               'kind': 'rodzaj',
131               'theme': 'motyw'
132               }
133
134
135 def add_book(db, book):
136     title = book.title
137     if book.html_file:
138         html_file = book.html_file.url
139         html_file_size = book.html_file.size
140     else:
141         html_file = html_file_size = None
142     if book.cover:
143         cover = book.cover.url
144     else:
145         cover = None
146     parent = book.parent_id
147     parent_number = book.parent_number
148     sort_key = book.sort_key
149     size_str = pretty_size(html_file_size)
150     authors = book.author_unicode()
151     db.execute(book_sql, locals())
152
153
154 def add_tag(db, tag):
155     category = categories[tag.category]
156     name = tag.name
157     sort_key = tag.sort_key
158
159     books = Book.tagged_top_level([tag])
160     book_ids = ','.join(str(b.id) for b in books)
161     db.execute(tag_sql, locals())