3
# Copyright (C) 2007-2009 Toms Bauģis <toms.baugis at gmail.com>
4
# Copyright (C) 2007 Patryk Zawadzki <patrys at pld-linux.org>
6
# This file is part of Project Hamster.
8
# Project Hamster is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
13
# Project Hamster is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
22
"""separate file for database operations"""
26
import sqlite3 as sqlite
29
logging.warn("Using sqlite2")
30
from pysqlite2 import dbapi2 as sqlite
32
logging.error("Neither sqlite3 nor pysqlite2 found")
39
from shutil import copy as copyfile
45
DB_FILE = 'hamster.db'
47
class Storage(storage.Storage):
48
con = None # Connection will be created on demand
51
Delayed setup so we don't do everything at the same time
53
if self.__setup.im_func.complete:
59
from configuration import runtime
61
db_file = runtime.database_path
62
db_path, _ = os.path.split(os.path.realpath(db_file))
64
if not os.path.exists(db_path):
66
os.makedirs(db_path, 0744)
67
except Exception, msg:
68
logging.error("could not create user dir (%s): %s" % (db_path, msg))
70
data_dir = runtime.data_dir
73
if not os.path.exists(db_file):
74
logging.info("Database not found in %s - installing default from %s!" % (db_file, data_dir))
75
copyfile(os.path.join(data_dir, DB_FILE), db_file)
77
#change also permissions - sometimes they are 444
79
os.chmod(db_file, 0664)
80
except Exception, msg:
81
logging.error("Could not change mode on %s!" % (db_file))
82
self.__setup.im_func.complete = True
87
__setup.complete = False
90
def __get_tags(self, autocomplete = None):
91
query = "select * from tags"
93
query += " where autocomplete='true'"
95
query += " order by name"
96
return self.fetchall(query)
98
def __get_tag_ids(self, tags):
99
"""look up tags by their name. create if not found"""
101
db_tags = self.fetchall("select * from tags where name in (%s)"
102
% ",".join(["?"] * len(tags)), tags) # bit of magic here - using sqlites bind variables
106
# check if any of tags needs ressurection
107
set_complete = [str(tag["id"]) for tag in db_tags if tag["autocomplete"] == "false"]
110
self.execute("update tags set autocomplete='true' where id in (%s)" % ", ".join(set_complete))
113
found_tags = [tag["name"] for tag in db_tags]
115
add = set(tags) - set(found_tags)
117
statement = "insert into tags(name) values(?)"
119
self.execute([statement] * len(add), [(tag,) for tag in add])
121
return self.__get_tag_ids(tags)[0], True # all done, recurse
123
return db_tags, changes
125
def __update_autocomplete_tags(self, tags):
126
tags = [tag.strip() for tag in tags.split(",") if tag.strip()] # split by comma
128
#first we will create new ones
129
tags, changes = self.__get_tag_ids(tags)
130
tags = [tag["id"] for tag in tags]
132
#now we will find which ones are gone from the list
134
SELECT b.id as id, b.autocomplete, count(a.fact_id) as occurences
136
LEFT JOIN fact_tags a on a.tag_id = b.id
137
WHERE b.id not in (%s)
139
""" % ",".join(["?"] * len(tags)) # bit of magic here - using sqlites bind variables
141
gone = self.fetchall(query, tags)
143
to_delete = [str(tag["id"]) for tag in gone if tag["occurences"] == 0]
144
to_uncomplete = [str(tag["id"]) for tag in gone if tag["occurences"] > 0 and tag["autocomplete"] == "true"]
147
self.execute("delete from tags where id in (%s)" % ", ".join(to_delete))
150
self.execute("update tags set autocomplete='false' where id in (%s)" % ", ".join(to_uncomplete))
152
return changes or len(to_delete + to_uncomplete) > 0
154
def __get_category_list(self):
155
return self.fetchall("SELECT * FROM categories ORDER BY category_order")
157
def __change_category(self, id, category_id):
158
# first check if we don't have an activity with same name before us
159
activity = self.fetchone("select name from activities where id = ?", (id, ))
160
existing_activity = self.__get_activity_by_name(activity['name'], category_id)
162
if id == existing_activity['id']: # we are already there, go home
165
if existing_activity: #ooh, we have something here!
166
# first move all facts that belong to movable activity to the new one
170
WHERE activity_id = ?
173
self.execute(update, (existing_activity['id'], id))
175
# and now get rid of our friend
176
self.__remove_activity(id)
179
query = "SELECT max(activity_order) + 1 FROM activities WHERE category_id = ?"
180
max_order = self.fetchone(query, (category_id, ))[0] or 1
184
SET category_id = ?, activity_order = ?
188
self.execute(statement, (category_id, max_order, id))
192
def __add_category(self, name):
193
order = self.fetchone("select max(category_order) + 1 from categories")[0] or 1
195
INSERT INTO categories (name, category_order)
198
self.execute(query, (name, order))
199
return self.__last_insert_rowid()
201
def __update_category(self, id, name):
202
if id > -1: # Update, and ignore unsorted, if that was somehow triggered
208
self.execute(update, (name, id))
210
def __move_activity(self, source_id, target_order, insert_after = True):
211
statement = "UPDATE activities SET activity_order = activity_order + 1"
214
statement += " WHERE activity_order > ?"
216
statement += " WHERE activity_order >= ?"
218
self.execute(statement, (target_order, ))
220
statement = "UPDATE activities SET activity_order = ? WHERE id = ?"
223
self.execute(statement, (target_order + 1, source_id))
225
self.execute(statement, (target_order, source_id))
229
def __get_activity_by_name(self, name, category_id = None, ressurect = True):
230
"""get most recent, preferably not deleted activity by it's name"""
234
SELECT a.id, a.name, a.deleted, coalesce(b.name, ?) as category
236
LEFT JOIN categories b ON category_id = b.id
237
WHERE lower(a.name) = lower(?)
239
ORDER BY a.deleted, a.id desc
243
res = self.fetchone(query, (_("Unsorted"), name, category_id))
246
SELECT a.id, a.name, a.deleted, coalesce(b.name, ?) as category
248
LEFT JOIN categories b ON category_id = b.id
249
WHERE lower(a.name) = lower(?)
250
ORDER BY a.deleted, a.id desc
253
res = self.fetchone(query, (_("Unsorted"), name, ))
256
# if the activity was marked as deleted, ressurect on first call
257
# and put in the unsorted category
258
if res['deleted'] and not ressurect:
263
SET deleted = null, category_id = -1
266
self.execute(update, (res['id'], ))
272
def __get_category_by_name(self, name):
273
"""returns category by it's name"""
276
SELECT id from categories
277
WHERE lower(name) = lower(?)
282
res = self.fetchone(query, (name, ))
289
def __get_fact(self, id):
292
a.start_time AS start_time,
293
a.end_time AS end_time,
294
a.description as description,
295
b.name AS name, b.id as activity_id,
296
coalesce(c.name, ?) as category, coalesce(c.id, -1) as category_id,
299
LEFT JOIN activities b ON a.activity_id = b.id
300
LEFT JOIN categories c ON b.category_id = c.id
301
LEFT JOIN fact_tags d ON d.fact_id = a.id
302
LEFT JOIN tags e ON e.id = d.tag_id
307
return self.__group_tags(self.fetchall(query, (_("Unsorted"), id)))[0]
309
def __group_tags(self, facts):
310
"""put the fact back together and move all the unique tags to an array"""
311
if not facts: return facts #be it None or whatever
314
for fact_id, fact_tags in itertools.groupby(facts, lambda f: f["id"]):
315
fact_tags = list(fact_tags)
317
# first one is as good as the last one
318
grouped_fact = fact_tags[0]
320
# we need dict so we can modify it (sqlite.Row is read only)
321
# in python 2.5, sqlite does not have keys() yet, so we hardcode them (yay!)
322
keys = ["id", "start_time", "end_time", "description", "name",
323
"activity_id", "category", "tag"]
324
grouped_fact = dict([(key, grouped_fact[key]) for key in keys])
326
grouped_fact["tags"] = [ft["tag"] for ft in fact_tags if ft["tag"]]
327
grouped_facts.append(grouped_fact)
330
def __get_last_activity(self):
331
facts = self.__get_todays_facts()
333
if facts and facts[-1]["end_time"] == None:
334
last_activity = facts[-1]
337
def __touch_fact(self, fact, end_time):
338
# tasks under one minute do not count
339
if end_time - fact['start_time'] < datetime.timedelta(minutes = 1):
340
self.__remove_fact(fact['id'])
342
end_time = end_time.replace(microsecond = 0)
348
self.execute(query, (end_time, fact['id']))
350
def __squeeze_in(self, start_time):
351
# tries to put task in the given date
352
# if there are conflicts, we will only truncate the ongoing task
353
# and replace it's end part with our activity
355
# we are checking if our start time is in the middle of anything
356
# or maybe there is something after us - so we know to adjust end time
357
# in the latter case go only few days ahead. everything else is madness, heh
361
LEFT JOIN activities b on b.id = a.activity_id
362
WHERE ((start_time < ? and end_time > ?)
363
OR (start_time > ? and start_time < ?))
367
fact = self.fetchone(query, (start_time,
370
start_time + dt.timedelta(seconds = 60 * 60 * 12)))
375
if fact["end_time"] and start_time > fact["start_time"]:
376
#we are in middle of a fact - truncate it to our start
377
self.execute("UPDATE facts SET end_time=? WHERE id=?",
378
(start_time, fact["id"]))
380
# hamster is second-aware, but the edit dialog naturally is not
381
# so when an ongoing task is being edited, the seconds get truncated
382
# and the start time will be before previous task's end time.
383
# so set our end time only if it is not about seconds
384
if fact["end_time"].replace(second = 0) > start_time:
385
end_time = fact["end_time"]
386
else: #otherwise we have found a task that is after us
387
end_time = fact["start_time"]
391
def __solve_overlaps(self, start_time, end_time):
392
"""finds facts that happen in given interval and shifts them to
393
make room for new fact"""
395
# this function is destructive - can't go with a wildcard
396
if not end_time or not start_time:
399
# activities that we are overlapping.
400
# second OR clause is for elimination - |new fact--|---old-fact--|--new fact|
402
SELECT a.*, b.name, c.name as category
404
LEFT JOIN activities b on b.id = a.activity_id
405
LEFT JOIN categories c on b.category_id = c.id
406
WHERE ((start_time < ? and end_time > ?)
407
OR (start_time < ? and end_time > ?))
409
OR ((start_time < ? and start_time > ?)
410
OR (end_time < ? and end_time > ?))
413
conflicts = self.fetchall(query, (start_time, start_time, end_time, end_time,
414
end_time, start_time, end_time, start_time))
416
for fact in conflicts:
417
# split - truncate until beginning of new entry and create new activity for end
418
if fact["start_time"] < start_time < fact["end_time"] and \
419
fact["start_time"] < end_time < fact["end_time"]:
421
logging.info("splitting %s" % fact["name"])
422
self.execute("""UPDATE facts
424
WHERE id = ?""", (start_time, fact["id"]))
425
fact_name = fact["name"]
426
new_fact = self.__add_fact(fact["name"],
427
"", # will create tags in the next step
432
tag_update = """INSERT INTO fact_tags(fact_id, tag_id)
436
self.execute(tag_update, (new_fact["id"], fact["id"])) #clone tags
439
elif fact["end_time"] and \
440
start_time < fact["start_time"] < end_time and \
441
start_time < fact["end_time"] < end_time:
442
logging.info("eliminating %s" % fact["name"])
443
self.__remove_fact(fact["id"])
446
elif start_time < fact["start_time"] < end_time:
447
logging.info("Overlapping start of %s" % fact["name"])
448
self.execute("UPDATE facts SET start_time=? WHERE id=?",
449
(end_time, fact["id"]))
452
elif start_time < fact["end_time"] < end_time:
453
logging.info("Overlapping end of %s" % fact["name"])
454
self.execute("UPDATE facts SET end_time=? WHERE id=?",
455
(start_time, fact["id"]))
458
def __add_fact(self, activity_name, tags, start_time = None,
459
end_time = None, category_name = None, description = None):
461
activity = stuff.parse_activity_input(activity_name)
463
tags = [tag.strip() for tag in tags.split(",") if tag.strip()] # split by comma
465
# explicitly stated takes precedence
466
activity.description = description or activity.description
468
tags = self.get_tag_ids(tags) #this will create any missing tags too
471
activity.category_name = category_name
473
activity.description = description #override
475
start_time = activity.start_time or start_time or datetime.datetime.now()
477
if start_time > datetime.datetime.now():
478
return None #no facts in future, please
480
start_time = start_time.replace(microsecond = 0)
481
end_time = activity.end_time or end_time
483
end_time = end_time.replace(microsecond = 0)
485
if not start_time or not activity.activity_name: # sanity check
488
# now check if maybe there is also a category
490
if activity.category_name:
491
category_id = self.__get_category_by_name(activity.category_name)
493
category_id = self.__add_category(activity.category_name)
495
# try to find activity
496
activity_id = self.__get_activity_by_name(activity.activity_name,
499
activity_id = self.__add_activity(activity.activity_name,
502
activity_id = activity_id['id']
505
# if we are working on +/- current day - check the last_activity
506
if (dt.datetime.now() - start_time <= dt.timedelta(days=1)):
508
# pull in previous facts
509
facts = self.__get_todays_facts()
512
if facts and facts[-1]["end_time"] == None:
515
if previous and previous['start_time'] < start_time:
516
# check if maybe that is the same one, in that case no need to restart
517
if previous["activity_id"] == activity_id \
518
and previous["tags"] == sorted([tag["name"] for tag in tags]) \
519
and previous["description"] == (description or ""):
522
# otherwise, if no description is added
523
# see if maybe it is too short to qualify as an activity
524
if not previous["description"] \
525
and 60 >= (start_time - previous['start_time']).seconds >= 0:
526
self.__remove_fact(previous['id'])
528
# now that we removed the previous one, see if maybe the one
529
# before that is actually same as the one we want to start
531
if len(facts) > 1 and 60 >= (start_time - facts[-2]['end_time']).seconds >= 0:
533
if before["activity_id"] == activity_id \
534
and before["tags"] == sorted([tag["name"] for tag in tags]):
535
# essentially same activity - resume it and return
541
self.execute(update, (before["id"],))
551
self.execute(update, (start_time, previous["id"]))
554
# done with the current activity, now we can solve overlaps
556
end_time = self.__squeeze_in(start_time)
558
self.__solve_overlaps(start_time, end_time)
561
# finally add the new entry
563
INSERT INTO facts (activity_id, start_time, end_time, description)
566
self.execute(insert, (activity_id, start_time, end_time, activity.description))
568
fact_id = self.__last_insert_rowid()
571
insert = ["insert into fact_tags(fact_id, tag_id) values(?, ?)"] * len(tags)
572
params = [(fact_id, tag["id"]) for tag in tags]
573
self.execute(insert, params)
577
def __last_insert_rowid(self):
578
return self.fetchone("SELECT last_insert_rowid();")[0]
581
def __get_todays_facts(self):
582
from configuration import conf
583
day_start = conf.get("day_start_minutes")
584
day_start = dt.time(day_start / 60, day_start % 60)
585
today = (dt.datetime.now() - dt.timedelta(hours = day_start.hour,
586
minutes = day_start.minute)).date()
587
return self.__get_facts(today)
590
def __get_facts(self, date, end_date = None, search_terms = ""):
593
a.start_time AS start_time,
594
a.end_time AS end_time,
595
a.description as description,
596
b.name AS name, b.id as activity_id,
597
coalesce(c.name, ?) as category,
600
LEFT JOIN activities b ON a.activity_id = b.id
601
LEFT JOIN categories c ON b.category_id = c.id
602
LEFT JOIN fact_tags d ON d.fact_id = a.id
603
LEFT JOIN tags e ON e.id = d.tag_id
604
WHERE (a.end_time >= ? OR a.end_time IS NULL) AND a.start_time <= ?
607
# let's see what we can do with search terms
608
# we will be looking in activity names, descriptions, categories and tags
609
# comma will be treated as OR
610
# space will be treated as AND or possible join
613
# split by comma and then by space and remove all extra spaces
614
or_bits = [[term.strip().lower().replace("'", "''") #striping removing case sensitivity and escaping quotes in term
615
for term in terms.strip().split(" ") if term.strip()]
616
for terms in search_terms.split(",") if terms.strip()]
618
def all_fields(term):
619
return """(lower(a.description) like '%%%(term)s%%'
620
or lower(b.name) = '%(term)s'
621
or lower(c.name) = '%(term)s'
622
or lower(e.name) = '%(term)s' )""" % dict(term = term)
625
search_query = "1<>1 " # will be building OR chain, so start with a false
627
for and_bits in or_bits:
628
if len(and_bits) == 1:
629
and_query = all_fields(and_bits[0])
631
and_query = "1=1 " # will be building AND chain, so start with a true
632
# if we have more than one word, go for "(a and b) or ab"
633
# to match two word tags
634
for bit1, bit2 in zip(and_bits, and_bits[1:]):
635
and_query += "and (%s and %s) or %s" % (all_fields(bit1),
637
all_fields("%s %s" % (bit1, bit2)))
639
search_query = "%s or (%s) " % (search_query, and_query)
641
query = "%s and (%s)" % (query, search_query)
645
query += " ORDER BY a.start_time, e.name"
646
end_date = end_date or date
648
from configuration import conf
649
day_start = conf.get("day_start_minutes")
650
day_start = dt.time(day_start / 60, day_start % 60)
652
split_time = day_start
653
datetime_from = dt.datetime.combine(date, split_time)
654
datetime_to = dt.datetime.combine(end_date, split_time) + dt.timedelta(days = 1)
656
facts = self.fetchall(query, (_("Unsorted"),
660
#first let's put all tags in an array
661
facts = self.__group_tags(facts)
665
# heuristics to assign tasks to proper days
667
# if fact has no end time, set the last minute of the day,
668
# or current time if fact has happened in last 24 hours
670
fact_end_time = fact["end_time"]
671
elif (dt.date.today() - fact["start_time"].date()) <= dt.timedelta(days=1):
672
fact_end_time = dt.datetime.now().replace(microsecond = 0)
674
fact_end_time = fact["start_time"]
676
fact_start_date = fact["start_time"].date() \
677
- dt.timedelta(1 if fact["start_time"].time() < split_time else 0)
678
fact_end_date = fact_end_time.date() \
679
- dt.timedelta(1 if fact_end_time.time() < split_time else 0)
680
fact_date_span = fact_end_date - fact_start_date
682
# check if the task spans across two dates
683
if fact_date_span.days == 1:
684
datetime_split = dt.datetime.combine(fact_end_date, split_time)
685
start_date_duration = datetime_split - fact["start_time"]
686
end_date_duration = fact_end_time - datetime_split
687
if start_date_duration > end_date_duration:
688
# most of the task was done during the previous day
689
fact_date = fact_start_date
691
fact_date = fact_end_date
693
# either doesn't span or more than 24 hrs tracked
694
# (in which case we give up)
695
fact_date = fact_start_date
697
if fact_date < date or fact_date > end_date:
698
# due to spanning we've jumped outside of given period
701
fact["date"] = fact_date
702
fact["delta"] = fact_end_time - fact["start_time"]
707
def __get_popular_categories(self):
708
"""returns categories used in the specified interval"""
710
SELECT coalesce(c.name, ?) as category, count(a.id) as popularity
712
LEFT JOIN activities b on a.activity_id = b.id
713
LEFT JOIN categories c on c.id = b.category_id
714
GROUP BY b.category_id
715
ORDER BY popularity desc
717
return self.fetchall(query, (_("Unsorted"), ))
719
def __remove_fact(self, fact_id):
720
statements = ["DELETE FROM fact_tags where fact_id = ?",
721
"DELETE FROM facts where id = ?"]
722
self.execute(statements, [(fact_id,)] * 2)
724
def __get_activities(self, category_id = None):
725
"""returns list of activities, if category is specified, order by name
726
otherwise - by activity_order"""
729
SELECT a.*, b.name as category
731
LEFT JOIN categories b on coalesce(b.id, -1) = a.category_id
732
WHERE category_id = ?
736
# unsorted entries we sort by name - others by ID
737
if category_id == -1:
738
query += "ORDER BY lower(a.name)"
740
query += "ORDER BY a.activity_order"
742
activities = self.fetchall(query, (category_id, ))
746
SELECT a.*, b.name as category
748
LEFT JOIN categories b on coalesce(b.id, -1) = a.category_id
749
WHERE deleted IS NULL
750
ORDER BY lower(a.name)
752
activities = self.fetchall(query)
756
def __get_autocomplete_activities(self, search):
757
"""returns list of activities for autocomplete,
758
activity names converted to lowercase"""
761
SELECT lower(a.name) AS name, b.name AS category
763
LEFT JOIN categories b ON coalesce(b.id, -1) = a.category_id
764
LEFT JOIN facts f ON a.id = f.activity_id
765
WHERE deleted IS NULL
766
AND a.name LIKE ? ESCAPE '\\'
768
ORDER BY max(f.start_time) DESC, lower(a.name)
771
search = search.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
772
activities = self.fetchall(query, (u'%s%%' % search, ))
776
def __remove_activity(self, id):
777
""" check if we have any facts with this activity and behave accordingly
778
if there are facts - sets activity to deleted = True
779
else, just remove it"""
781
query = "select count(*) as count from facts where activity_id = ?"
782
bound_facts = self.fetchone(query, (id,))['count']
785
self.execute("UPDATE activities SET deleted = 1 WHERE id = ?", (id,))
787
self.execute("delete from activities where id = ?", (id,))
789
def __remove_category(self, id):
790
"""move all activities to unsorted and remove category"""
792
update = "update activities set category_id = -1 where category_id = ?"
793
self.execute(update, (id, ))
795
self.execute("delete from categories where id = ?", (id, ))
798
def __swap_activities(self, id1, priority1, id2, priority2):
799
""" swaps nearby activities """
800
# TODO - 2 selects and 2 updates is wrong we could live without selects
801
self.execute(["update activities set activity_order = ? where id = ?",
802
"update activities set activity_order = ? where id = ?"],
803
[(priority1, id2), (priority2, id1)])
805
def __add_activity(self, name, category_id = None):
806
# first check that we don't have anything like that yet
807
activity = self.__get_activity_by_name(name, category_id)
809
return activity['id']
811
#now do the create bit
812
category_id = category_id or -1
813
new_order = self.fetchone("select max(activity_order) + 1 from activities")[0] or 1
816
INSERT INTO activities (name, category_id, activity_order)
819
self.execute(query, (name, category_id, new_order))
820
return self.__last_insert_rowid()
822
def __update_activity(self, id, name, category_id):
829
self.execute(query, (name, category_id, id))
831
""" Here be dragons (lame connection/cursor wrappers) """
832
def get_connection(self):
833
from configuration import runtime
835
db_file = runtime.database_path
836
self.con = sqlite.connect(db_file, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
837
self.con.row_factory = sqlite.Row
841
connection = property(get_connection, None)
843
def fetchall(self, query, params = None):
844
from configuration import runtime
847
con = self.connection
850
logging.debug("%s %s" % (query, params))
853
cur.execute(query, params)
862
def fetchone(self, query, params = None):
863
res = self.fetchall(query, params)
869
def execute(self, statement, params = ()):
871
execute sql statement. optionally you can give multiple statements
872
to save on cursor creation and closure
874
from configuration import runtime
877
con = self.__con or self.connection
878
cur = self.__cur or con.cursor()
880
if isinstance(statement, list) == False: #we kind of think that we will get list of instructions
881
statement = [statement]
884
if isinstance(statement, list):
885
for i in range(len(statement)):
886
logging.debug("%s %s" % (statement[i], params[i]))
888
res = cur.execute(statement[i], params[i])
893
runtime.register_modification()
896
def start_transaction(self):
897
# will give some hints to execute not to close or commit anything
898
self.__con = self.connection
899
self.__cur = self.__con.cursor()
901
def end_transaction(self):
905
from configuration import runtime
906
runtime.register_modification()
908
def run_fixtures(self):
909
self.start_transaction()
912
work_category = {"name": _("Work"),
913
"entries": [_("Reading news"),
914
_("Checking stocks"),
915
_("Super secret project X"),
916
_("World domination")]}
918
nonwork_category = {"name": _("Day-to-day"),
919
"entries": [_("Lunch"),
920
_("Watering flowers"),
921
_("Doing handstands")]}
923
"""upgrade DB to hamster version"""
924
version = self.fetchone("SELECT version FROM version")["version"]
928
"""moving from fact_date, fact_time to start_time, end_time"""
931
CREATE TABLE facts_new
932
(id integer primary key,
934
start_time varchar2(12),
935
end_time varchar2(12))
939
INSERT INTO facts_new
940
(id, activity_id, start_time)
941
SELECT id, activity_id, fact_date || fact_time
945
self.execute("DROP TABLE facts")
946
self.execute("ALTER TABLE facts_new RENAME TO facts")
948
# run through all facts and set the end time
949
# if previous fact is not on the same date, then it means that it was the
950
# last one in previous, so remove it
951
# this logic saves our last entry from being deleted, which is good
952
facts = self.fetchall("""
953
SELECT id, activity_id, start_time,
954
substr(start_time,1, 8) start_date
962
if prev_fact['start_date'] == fact['start_date']:
963
self.execute("UPDATE facts SET end_time = ? where id = ?",
964
(fact['start_time'], prev_fact['id']))
966
#otherwise that's the last entry of the day - remove it
967
self.execute("DELETE FROM facts WHERE id = ?", (prev_fact["id"],))
971
#it was kind of silly not to have datetimes in first place
974
CREATE TABLE facts_new
975
(id integer primary key,
977
start_time timestamp,
982
INSERT INTO facts_new
983
(id, activity_id, start_time, end_time)
984
SELECT id, activity_id,
985
substr(start_time,1,4) || "-"
986
|| substr(start_time, 5, 2) || "-"
987
|| substr(start_time, 7, 2) || " "
988
|| substr(start_time, 9, 2) || ":"
989
|| substr(start_time, 11, 2) || ":00",
990
substr(end_time,1,4) || "-"
991
|| substr(end_time, 5, 2) || "-"
992
|| substr(end_time, 7, 2) || " "
993
|| substr(end_time, 9, 2) || ":"
994
|| substr(end_time, 11, 2) || ":00"
998
self.execute("DROP TABLE facts")
999
self.execute("ALTER TABLE facts_new RENAME TO facts")
1002
#adding categories table to categorize activities
1004
#adding the categories table
1006
CREATE TABLE categories
1007
(id integer primary key,
1009
color_code varchar2(50),
1010
category_order integer)
1013
# adding default categories, and make sure that uncategorized stays on bottom for starters
1014
# set order to 2 in case, if we get work in next lines
1016
INSERT INTO categories
1017
(id, name, category_order)
1019
""", (nonwork_category["name"],))
1021
#check if we have to create work category - consider work everything that has been determined so, and is not deleted
1022
work_activities = self.fetchone("""
1023
SELECT count(*) as work_activities
1025
WHERE deleted is null and work=1;
1026
""")['work_activities']
1028
if work_activities > 0:
1030
INSERT INTO categories
1031
(id, name, category_order)
1033
""", (work_category["name"],))
1035
# now add category field to activities, before starting the move
1036
self.execute(""" ALTER TABLE activities
1037
ADD COLUMN category_id integer;
1043
# first remove all deleted activities with no instances in facts
1045
DELETE FROM activities
1047
AND id not in(select activity_id from facts);
1051
# moving work / non-work to appropriate categories
1052
# exploit false/true = 0/1 thing
1053
self.execute(""" UPDATE activities
1054
SET category_id = work + 1
1055
WHERE deleted is null
1058
#finally, set category to -1 where there is none
1059
self.execute(""" UPDATE activities
1060
SET category_id = -1
1061
WHERE category_id is null
1064
# drop work column and forget value of deleted
1065
# previously deleted records are now unsorted ones
1066
# user will be able to mark them as deleted again, in which case
1067
# they won't appear in autocomplete, or in categories
1068
# ressurection happens, when user enters the exact same name
1070
CREATE TABLE activities_new (id integer primary key,
1072
activity_order integer,
1074
category_id integer);
1078
INSERT INTO activities_new
1079
(id, name, activity_order, category_id)
1080
SELECT id, name, activity_order, category_id
1084
self.execute("DROP TABLE activities")
1085
self.execute("ALTER TABLE activities_new RENAME TO activities")
1088
self.execute("ALTER TABLE facts add column description varchar2")
1091
# facts table could use an index
1092
self.execute("CREATE INDEX idx_facts_start_end ON facts(start_time, end_time)")
1093
self.execute("CREATE INDEX idx_facts_start_end_activity ON facts(start_time, end_time, activity_id)")
1096
self.execute("""CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
1098
autocomplete BOOL DEFAULT true)""")
1099
self.execute("CREATE INDEX idx_tags_name ON tags(name)")
1101
self.execute("CREATE TABLE fact_tags(fact_id integer, tag_id integer)")
1102
self.execute("CREATE INDEX idx_fact_tags_fact ON fact_tags(fact_id)")
1103
self.execute("CREATE INDEX idx_fact_tags_tag ON fact_tags(tag_id)")
1105
# at the happy end, update version number
1106
if version < current_version:
1107
#lock down current version
1108
self.execute("UPDATE version SET version = %d" % current_version)
1110
"""we start with an empty database and then populate with default
1111
values. This way defaults can be localized!"""
1113
category_count = self.fetchone("select count(*) from categories")[0]
1115
if category_count == 0:
1116
work_cat_id = self.__add_category(work_category["name"])
1117
for entry in work_category["entries"]:
1118
self.__add_activity(entry, work_cat_id)
1120
nonwork_cat_id = self.__add_category(nonwork_category["name"])
1121
for entry in nonwork_category["entries"]:
1122
self.__add_activity(entry, nonwork_cat_id)
1125
self.end_transaction()