1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 """
20 Implementation of timeline database with xml file storage.
21 """
22
23
24 import re
25 import os.path
26 from os.path import abspath
27 import base64
28 import StringIO
29 from xml.sax.saxutils import escape as xmlescape
30
31 import wx
32
33 from timelinelib.db.backends.memory import MemoryDB
34 from timelinelib.db.exceptions import TimelineIOError
35 from timelinelib.db.objects import Category
36 from timelinelib.db.objects import Container
37 from timelinelib.db.objects import Event
38 from timelinelib.db.objects import Subevent
39 from timelinelib.db.objects import TimePeriod
40 from timelinelib.db.utils import safe_write
41 from timelinelib.meta.version import get_version
42 from timelinelib.time import WxTimeType
43 from timelinelib.utils import ex_msg
44 from timelinelib.xml.parser import ANY
45 from timelinelib.xml.parser import OPTIONAL
46 from timelinelib.xml.parser import parse
47 from timelinelib.xml.parser import parse_fn_store
48 from timelinelib.xml.parser import SINGLE
49 from timelinelib.xml.parser import Tag
50
51
52 ENCODING = "utf-8"
53 INDENT1 = " "
54 INDENT2 = " "
55 INDENT3 = " "
56
57
58
71 return wrapper
72
73
75 """Thrown if parsing of data read from file fails."""
76 pass
77
78
80
81 - def __init__(self, path, load=True, use_wide_date_range=False):
88
92
95
98
114
116 """
117 Load timeline data from the file that this timeline points to.
118
119 This should only be done once when this class is created.
120
121 The data is stored internally until we do a save.
122
123 If a read error occurs a TimelineIOError will be raised.
124 """
125 if not os.path.exists(self.path):
126
127 return
128 try:
129
130 partial_schema = Tag("timeline", SINGLE, None, [
131 Tag("version", SINGLE, self._parse_version)
132 ])
133 tmp_dict = {
134 "partial_schema": partial_schema,
135 "category_map": {},
136 "hidden_categories": [],
137 }
138 self.disable_save()
139 parse(self.path, partial_schema, tmp_dict)
140 self.enable_save(call_save=False)
141 except Exception, e:
142 msg = _("Unable to read timeline data from '%s'.")
143 whole_msg = (msg + "\n\n%s") % (abspath(self.path), ex_msg(e))
144 raise TimelineIOError(whole_msg)
145
147 match = re.search(r"^(\d+).(\d+).(\d+)(dev.*)?$", text)
148 if match:
149 (x, y, z) = (int(match.group(1)), int(match.group(2)),
150 int(match.group(3)))
151 tmp_dict["version"] = (x, y, z)
152 self._create_rest_of_schema(tmp_dict)
153 else:
154 raise ParseException("Could not parse version number from '%s'."
155 % text)
156
158 """
159 Ensure all versions of the xml format can be parsed with this schema.
160
161 tmp_dict["version"] can be used to create different schemas depending
162 on the version.
163 """
164 tmp_dict["partial_schema"].add_child_tags([
165 Tag("categories", SINGLE, None, [
166 Tag("category", ANY, self._parse_category, [
167 Tag("name", SINGLE, parse_fn_store("tmp_name")),
168 Tag("color", SINGLE, parse_fn_store("tmp_color")),
169 Tag("font_color", OPTIONAL, parse_fn_store("tmp_font_color")),
170 Tag("parent", OPTIONAL, parse_fn_store("tmp_parent")),
171 ])
172 ]),
173 Tag("events", SINGLE, None, [
174 Tag("event", ANY, self._parse_event, [
175 Tag("start", SINGLE, parse_fn_store("tmp_start")),
176 Tag("end", SINGLE, parse_fn_store("tmp_end")),
177 Tag("text", SINGLE, parse_fn_store("tmp_text")),
178 Tag("fuzzy", OPTIONAL, parse_fn_store("tmp_fuzzy")),
179 Tag("locked", OPTIONAL, parse_fn_store("tmp_locked")),
180 Tag("ends_today", OPTIONAL, parse_fn_store("tmp_ends_today")),
181 Tag("category", OPTIONAL,
182 parse_fn_store("tmp_category")),
183 Tag("description", OPTIONAL,
184 parse_fn_store("tmp_description")),
185 Tag("alert", OPTIONAL,
186 parse_fn_store("tmp_alert")),
187 Tag("hyperlink", OPTIONAL,
188 parse_fn_store("tmp_hyperlink")),
189 Tag("icon", OPTIONAL,
190 parse_fn_store("tmp_icon")),
191 ])
192 ]),
193 Tag("view", SINGLE, None, [
194 Tag("displayed_period", OPTIONAL,
195 self._parse_displayed_period, [
196 Tag("start", SINGLE, parse_fn_store("tmp_start")),
197 Tag("end", SINGLE, parse_fn_store("tmp_end")),
198 ]),
199 Tag("hidden_categories", OPTIONAL,
200 self._parse_hidden_categories, [
201 Tag("name", ANY, self._parse_hidden_category),
202 ]),
203 ]),
204 ])
205
207 name = tmp_dict.pop("tmp_name")
208 color = parse_color(tmp_dict.pop("tmp_color"))
209 font_color = self._parse_optional_color(tmp_dict, "tmp_font_color")
210 parent_name = tmp_dict.pop("tmp_parent", None)
211 if parent_name:
212 parent = tmp_dict["category_map"].get(parent_name, None)
213 if parent is None:
214 raise ParseException("Parent category '%s' not found." % parent_name)
215 else:
216 parent = None
217 category = Category(name, color, font_color, True, parent=parent)
218 tmp_dict["category_map"][name] = category
219 self.save_category(category)
220
222 start = self._parse_time(tmp_dict.pop("tmp_start"))
223 end = self._parse_time(tmp_dict.pop("tmp_end"))
224 text = tmp_dict.pop("tmp_text")
225 fuzzy = self._parse_optional_bool(tmp_dict, "tmp_fuzzy")
226 locked = self._parse_optional_bool(tmp_dict, "tmp_locked")
227 ends_today = self._parse_optional_bool(tmp_dict, "tmp_ends_today")
228 category_text = tmp_dict.pop("tmp_category", None)
229 if category_text is None:
230 category = None
231 else:
232 category = tmp_dict["category_map"].get(category_text, None)
233 if category is None:
234 raise ParseException("Category '%s' not found." % category_text)
235 description = tmp_dict.pop("tmp_description", None)
236 alert_string = tmp_dict.pop("tmp_alert", None)
237 alert = self._parse_alert_string(alert_string)
238 icon_text = tmp_dict.pop("tmp_icon", None)
239 if icon_text is None:
240 icon = None
241 else:
242 icon = parse_icon(icon_text)
243 hyperlink = tmp_dict.pop("tmp_hyperlink", None)
244 if self._is_container_event(text):
245 cid, text = self._extract_container_id(text)
246 event = Container(self.get_time_type(), start, end, text, category, cid=cid)
247 elif self._is_subevent(text):
248 cid, text = self._extract_subid(text)
249 event = Subevent(self.get_time_type(), start, end, text, category, cid=cid)
250 else:
251 event = Event(self.get_time_type(), start, end, text, category, fuzzy, locked, ends_today)
252 event.set_data("description", description)
253 event.set_data("icon", icon)
254 event.set_data("alert", alert)
255 event.set_data("hyperlink", hyperlink)
256 self.save_event(event)
257
262
274
276 return text.startswith("[")
277
279 return text.startswith("(")
280
282 str_id, text = text.split("]", 1)
283 try:
284 str_id = str_id[1:]
285 id = int(str_id)
286 except:
287 id = -1
288 return id, text
289
291 id, text = text.split(")", 1)
292 try:
293 id = int(id[1:])
294 except:
295 id = -1
296 return id, text
297
299 if tmp_dict.has_key(id):
300 return tmp_dict.pop(id) == "True"
301 else:
302 return False
303
305 if tmp_dict.has_key(id):
306 return parse_color(tmp_dict.pop(id))
307 else:
308 return (0, 0, 0)
309
311 start = self._parse_time(tmp_dict.pop("tmp_start"))
312 end = self._parse_time(tmp_dict.pop("tmp_end"))
313 self._set_displayed_period(TimePeriod(self.get_time_type(), start, end))
314
320
322 self._set_hidden_categories(tmp_dict.pop("hidden_categories"))
323
325 self._make_sure_subevets_are_saved_last()
326 safe_write(self.path, ENCODING, self._write_xml_doc)
327
333
337
343 _write_timeline = wrap_in_tag(_write_timeline, "timeline")
344
346 def write_with_parent(categories, parent):
347 for cat in categories:
348 if cat.parent == parent:
349 self._write_category(file, cat)
350 write_with_parent(categories, cat)
351 write_with_parent(self.get_categories(), None)
352 _write_categories = wrap_in_tag(_write_categories, "categories", INDENT1)
353
355 write_simple_tag(file, "name", cat.name, INDENT3)
356 write_simple_tag(file, "color", color_string(cat.color), INDENT3)
357 write_simple_tag(file, "font_color", color_string(cat.font_color), INDENT3)
358 if cat.parent:
359 write_simple_tag(file, "parent", cat.parent.name, INDENT3)
360 _write_category = wrap_in_tag(_write_category, "category", INDENT2)
361
365 _write_events = wrap_in_tag(_write_events, "events", INDENT1)
366
368 write_simple_tag(file, "start",
369 self._time_string(evt.time_period.start_time), INDENT3)
370 write_simple_tag(file, "end",
371 self._time_string(evt.time_period.end_time), INDENT3)
372 if evt.is_container():
373 write_simple_tag(file, "text", "[%d]%s " % (evt.cid(), evt.text), INDENT3)
374 elif evt.is_subevent():
375 write_simple_tag(file, "text", "(%d)%s " % (evt.cid(), evt.text), INDENT3)
376 else:
377 write_simple_tag(file, "text", evt.text, INDENT3)
378 write_simple_tag(file, "fuzzy", "%s" % evt.fuzzy, INDENT3)
379 write_simple_tag(file, "locked", "%s" % evt.locked, INDENT3)
380 write_simple_tag(file, "ends_today", "%s" % evt.ends_today, INDENT3)
381 if evt.category is not None:
382 write_simple_tag(file, "category", evt.category.name, INDENT3)
383 if evt.get_data("description") is not None:
384 write_simple_tag(file, "description", evt.get_data("description"),
385 INDENT3)
386 alert = evt.get_data("alert")
387 if alert is not None:
388 write_simple_tag(file, "alert", self.alert_string(alert),
389 INDENT3)
390 hyperlink = evt.get_data("hyperlink")
391 if hyperlink is not None:
392 write_simple_tag(file, "hyperlink", hyperlink, INDENT3)
393 if evt.get_data("icon") is not None:
394 icon_text = icon_string(evt.get_data("icon"))
395 write_simple_tag(file, "icon", icon_text, INDENT3)
396 _write_event = wrap_in_tag(_write_event, "event", INDENT2)
397
402 _write_view = wrap_in_tag(_write_view, "view", INDENT1)
403
410 _write_displayed_period = wrap_in_tag(_write_displayed_period,
411 "displayed_period", INDENT2)
412
416 _write_hidden_categories = wrap_in_tag(_write_hidden_categories,
417 "hidden_categories", INDENT2)
418
419
429
430
432 """
433 Expected format 'True' or 'False'.
434
435 Return True or False.
436 """
437 if bool_string == "True":
438 return True
439 elif bool_string == "False":
440 return False
441 else:
442 raise ParseException("Unknown boolean '%s'" % bool_string)
443
444
446 return "%i,%i,%i" % color
447
448
450 """
451 Expected format 'r,g,b'.
452
453 Return a tuple (r, g, b).
454 """
455 def verify_255_number(num):
456 if num < 0 or num > 255:
457 raise ParseException("Color number not in range [0, 255], "
458 "color string = '%s'" % color_string)
459 match = re.search(r"^(\d+),(\d+),(\d+)$", color_string)
460 if match:
461 r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
462 verify_255_number(r)
463 verify_255_number(g)
464 verify_255_number(b)
465 return (r, g, b)
466 else:
467 raise ParseException("Color not on correct format, color string = '%s'"
468 % color_string)
469
471 output = StringIO.StringIO()
472 image = wx.ImageFromBitmap(bitmap)
473 image.SaveStream(output, wx.BITMAP_TYPE_PNG)
474 return base64.b64encode(output.getvalue())
475
476
478 """
479 Expected format: base64 encoded png image.
480
481 Return a wx.Bitmap.
482 """
483 try:
484 input = StringIO.StringIO(base64.b64decode(string))
485 image = wx.ImageFromStream(input, wx.BITMAP_TYPE_PNG)
486 return image.ConvertToBitmap()
487 except:
488 raise ParseException("Could not parse icon from '%s'." % string)
489