Package Gnumed :: Package timelinelib :: Package canvas :: Package data :: Module db
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.canvas.data.db

  1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg 
  2  # 
  3  # This file is part of Timeline. 
  4  # 
  5  # Timeline is free software: you can redistribute it and/or modify 
  6  # it under the terms of the GNU General Public License as published by 
  7  # the Free Software Foundation, either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Timeline is distributed in the hope that it will be useful, 
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  # GNU General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU General Public License 
 16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>. 
 17   
 18   
 19  import collections 
 20  import contextlib 
 21   
 22  from timelinelib.calendar.gregorian.timetype import GregorianTimeType 
 23  from timelinelib.canvas.data.exceptions import TimelineIOError 
 24  from timelinelib.canvas.data.immutable import ImmutableDB 
 25  from timelinelib.canvas.data import Category 
 26  from timelinelib.canvas.data import Container 
 27  from timelinelib.canvas.data import Era 
 28  from timelinelib.canvas.data import Eras 
 29  from timelinelib.canvas.data import Event 
 30  from timelinelib.canvas.data import Milestone 
 31  from timelinelib.canvas.data import Subevent 
 32  from timelinelib.canvas.data import TimePeriod 
 33  from timelinelib.canvas.data.transactions import Transactions 
 34  from timelinelib.general.observer import Observable 
 35   
 36   
 37  # A category was added, edited, or deleted 
 38  STATE_CHANGE_CATEGORY = 1 
 39  # Something happened that changed the state of the timeline 
 40  STATE_CHANGE_ANY = 2 
41 42 43 -class MemoryDB(Observable):
44
45 - def __init__(self):
46 Observable.__init__(self) 47 self._id_counter = 0 48 self._transactions = Transactions(ImmutableDB()) 49 self._transactions.listen_for_any(self._transaction_committed) 50 self.path = "" 51 self.displayed_period = None 52 self._hidden_category_ids = [] 53 self.time_type = GregorianTimeType() 54 self.saved_now = self.time_type.now() 55 self.readonly = False 56 self._save_callback = None 57 self._should_lock = False 58 self._current_query = None
59
60 - def new_category(self, **kwargs):
61 return self._create_wrapper(Category, **kwargs)
62
63 - def new_milestone(self, **kwargs):
64 return self._create_wrapper(Milestone, **kwargs)
65
66 - def new_era(self, **kwargs):
67 return self._create_wrapper(Era, **kwargs)
68
69 - def new_event(self, **kwargs):
70 return self._create_wrapper(Event, **kwargs)
71
72 - def new_container(self, **kwargs):
73 return self._create_wrapper(Container, **kwargs)
74
75 - def new_subevent(self, **kwargs):
76 return self._create_wrapper(Subevent, **kwargs)
77
78 - def _create_wrapper(self, wrapper_class, **kwargs):
79 wrapper = wrapper_class(db=self) 80 if hasattr(wrapper, "time_period") and "time_period" not in kwargs: 81 now = self.time_type.now() 82 kwargs["time_period"] = TimePeriod(now, now) 83 if "container" in kwargs: 84 container = kwargs.pop("container") 85 else: 86 container = None 87 for name, value in kwargs.items(): 88 setattr(wrapper, name, value) 89 if container is not None: 90 wrapper.container = container 91 return wrapper
92
93 - def next_id(self):
94 self._id_counter += 1 95 return self._id_counter
96
97 - def transaction(self, name):
98 return self._transactions.new(name)
99
100 - def clear_transactions(self):
101 self._transactions.clear()
102
103 - def transactions_status(self):
104 return self._transactions.status
105
106 - def display_in_canvas(self, canvas):
107 canvas.SetTimeline(self)
108
109 - def is_saved(self):
110 return self._save_callback is not None
111
112 - def get_should_lock(self):
113 return self._should_lock
114
115 - def set_should_lock(self, should_lock):
116 self._should_lock = should_lock
117
118 - def register_save_callback(self, callback):
119 self._save_callback = callback
120
121 - def get_time_type(self):
122 return self.time_type
123
124 - def set_time_type(self, time_type):
125 self.time_type = time_type 126 if time_type is not None: 127 self.saved_now = time_type.now()
128
129 - def is_read_only(self):
130 return self.readonly
131
132 - def set_readonly(self):
133 self.readonly = True 134 self._notify(STATE_CHANGE_ANY)
135
136 - def search(self, search_string):
137 return _generic_event_search(self.get_all_events(), search_string)
138
139 - def get_events(self, time_period):
140 return self._get_events(lambda immutable_event: 141 immutable_event.time_period.inside_period(time_period) 142 )
143
144 - def get_all_events(self):
145 return self._get_events(lambda immutable_event: True)
146
147 - def get_max_sort_order(self):
148 max = -1 149 for id_, immutable_value in self._transactions.value.milestones: 150 sort_order = immutable_value["sort_order"] 151 if sort_order > max: 152 max = sort_order 153 for id_, immutable_value in self._transactions.value.events: 154 sort_order = immutable_value["sort_order"] 155 if sort_order > max: 156 max = sort_order 157 return max
158
159 - def _get_events(self, criteria_fn):
160 with self._query() as query: 161 return ( 162 self._get_milestones(criteria_fn) + 163 sort_events(self.get_containers() + [ 164 query.get_event(id_) 165 for id_, immutable_event 166 in self._transactions.value.events 167 if criteria_fn(immutable_event) 168 ]) 169 )
170
171 - def _get_milestones(self, criteria_fn):
172 with self._query() as query: 173 return [ 174 query.get_milestone(id_) 175 for id_, immutable_milestone 176 in self._transactions.value.milestones 177 if criteria_fn(immutable_milestone) 178 ]
179
180 - def get_first_event(self):
181 if len(self._transactions.value.events) == 0: 182 return None 183 else: 184 id_, immutable_event = min( 185 self._transactions.value.events, 186 key=lambda id__immutable_event: 187 id__immutable_event[1].time_period.start_time 188 ) 189 with self._query() as query: 190 return query.get_event(id_)
191
192 - def get_last_event(self):
193 if len(self._transactions.value.events) == 0: 194 return None 195 else: 196 id_, immutable_event = max( 197 self._transactions.value.events, 198 key=lambda id__immutable_event1: 199 id__immutable_event1[1].time_period.end_time 200 ) 201 with self._query() as query: 202 return query.get_event(id_)
203
204 - def save_events(self, events):
205 try: 206 with self.transaction("Save events"): 207 for event in events: 208 event.db = self 209 event.save() 210 except Exception as e: 211 raise TimelineIOError("Saving event failed: %s" % e)
212
213 - def save_event(self, event):
214 self.save_events([event])
215
216 - def delete_event(self, event_or_id):
217 try: 218 if isinstance(event_or_id, Event): 219 event = event_or_id 220 else: 221 event = self.find_event_with_id(event_or_id) 222 event.db = self 223 event.delete() 224 except Exception as e: 225 raise TimelineIOError("Deleting event failed: %s" % e)
226
227 - def get_all_eras(self):
228 return self._get_eras().get_all()
229
230 - def get_all_periods(self):
231 return self._get_eras().get_all_periods()
232
233 - def _get_eras(self):
234 with self._query() as query: 235 return Eras( 236 now_func=self.time_type.now, 237 eras=[ 238 query.get_era(id_) 239 for id_, immutable_era 240 in self._transactions.value.eras 241 ] 242 )
243
244 - def save_era(self, era):
245 try: 246 era.db = self 247 era.save() 248 except Exception as e: 249 raise TimelineIOError("Saving Era failed: %s" % e)
250
251 - def delete_era(self, era):
252 try: 253 era.db = self 254 era.delete() 255 except Exception as e: 256 raise TimelineIOError("Deleting Era failed: %s" % e)
257
258 - def get_categories(self):
259 with self._query() as query: 260 return [ 261 query.get_category(id_) 262 for id_, immutable_category 263 in self._transactions.value.categories 264 ]
265
266 - def get_containers(self):
267 with self._query() as query: 268 return [ 269 query.get_container(id_) 270 for id_, immutable_container 271 in self._transactions.value.containers 272 ]
273
274 - def save_category(self, category):
275 try: 276 category.db = self 277 category.save() 278 except Exception as e: 279 raise TimelineIOError("Saving category failed: %s" % e)
280
281 - def get_category_by_name(self, name):
282 with self._query() as query: 283 for id_, immutable_category in self._transactions.value.categories: 284 if immutable_category.name == name: 285 return query.get_category(id_) 286 return None
287
288 - def get_category_by_id(self, id_):
289 for category in self.get_categories(): 290 if category.id == id_: 291 return category
292
293 - def delete_category(self, category_or_id):
294 try: 295 if isinstance(category_or_id, Category): 296 category = category_or_id 297 else: 298 with self._query() as query: 299 category = query.get_category(category_or_id) 300 if category.id in self._hidden_category_ids: 301 self._hidden_category_ids.remove(category.id) 302 category.db = self 303 category.delete() 304 except Exception as e: 305 raise TimelineIOError("Deleting category failed: %s" % e)
306
307 - def get_saved_now(self):
308 return self.saved_now
309
310 - def set_saved_now(self, time):
311 self.saved_now = time 312 self.time_type.set_saved_now(time)
313
314 - def load_view_properties(self, view_properties):
315 view_properties.displayed_period = self.displayed_period 316 for cat in self.get_categories(): 317 visible = cat.id not in self._hidden_category_ids 318 view_properties.set_category_visible(cat, visible)
319
320 - def save_view_properties(self, view_properties):
321 if view_properties.displayed_period is not None: 322 if not view_properties.displayed_period.is_period(): 323 raise TimelineIOError(_("Displayed period must be > 0.")) 324 self.displayed_period = view_properties.displayed_period 325 self._hidden_category_ids = [] 326 for cat in self.get_categories(): 327 if not view_properties.is_category_visible(cat): 328 self._hidden_category_ids.append(cat.id) 329 self._save()
330
331 - def place_event_after_event(self, event_to_place, target_event):
332 self._place_event( 333 lambda index_to_place, index_target: index_to_place < index_target, 334 event_to_place.id, 335 target_event.id 336 )
337
338 - def place_event_before_event(self, event_to_place, target_event):
339 self._place_event( 340 lambda index_to_place, index_target: index_to_place > index_target, 341 event_to_place.id, 342 target_event.id 343 )
344
345 - def _place_event(self, validate_index, id_to_place, id_target):
346 all_events = [ 347 event 348 for event 349 in self.get_all_events() 350 if not event.is_subevent() 351 ] 352 for events in self._get_event_lists(all_events): 353 if self._move(events, validate_index, id_to_place, id_target): 354 with self.transaction("Move event"): 355 EventSorter().save_sort_order(all_events) 356 return
357
358 - def _get_event_lists(self, all_events):
359 yield all_events 360 for event in all_events: 361 if event.is_container(): 362 yield event.subevents
363
364 - def _move(self, events, validate_index, id_to_place, id_target):
365 index_to_place = None 366 index_target = None 367 for index, event in enumerate(events): 368 if event.id == id_to_place: 369 index_to_place = index 370 if event.id == id_target: 371 index_target = index 372 if index_to_place is None: 373 return False 374 if index_target is None: 375 return False 376 if validate_index(index_to_place, index_target): 377 events.insert(index_target, events.pop(index_to_place)) 378 return True 379 return False
380
381 - def undo(self):
382 index = self._get_undo_index() 383 if index is not None: 384 self._transactions.move(index)
385
386 - def redo(self):
387 index = self._get_redo_index() 388 if index is not None: 389 self._transactions.move(index)
390
391 - def undo_enabled(self):
392 return not self.is_read_only() and self._get_undo_index() is not None
393
394 - def redo_enabled(self):
395 return not self.is_read_only() and self._get_redo_index() is not None
396
397 - def _get_undo_index(self):
398 index, is_in_transaction, history = self._transactions.status 399 if index > 0: 400 return index - 1 401 else: 402 return None
403
404 - def _get_redo_index(self):
405 index, is_in_transaction, history = self._transactions.status 406 if index < len(history) - 1: 407 return index + 1 408 else: 409 return None
410
411 - def find_event_with_ids(self, ids):
412 with self._query() as query: 413 events = [self.find_event_with_id(id_) for id_ in ids] 414 events = [e for e in events if e is not None] 415 return events
416
417 - def find_event_with_id(self, event_id):
418 with self._query() as query: 419 if query.event_exists(event_id): 420 return query.get_event(event_id) 421 if query.container_exists(event_id): 422 return query.get_container(event_id) 423 if query.milestone_exists(event_id): 424 return query.get_milestone(event_id)
425
426 - def _transaction_committed(self):
427 self._save() 428 self._notify(STATE_CHANGE_ANY)
429
430 - def _save(self):
431 if self._save_callback is not None: 432 self._save_callback()
433
434 - def get_displayed_period(self):
435 """ 436 Inheritors can call this method to get the displayed period used in 437 load_view_properties and save_view_properties. 438 """ 439 return self.displayed_period
440
441 - def set_displayed_period(self, period):
442 """ 443 Inheritors can call this method to set the displayed period used in 444 load_view_properties and save_view_properties. 445 """ 446 self.displayed_period = period
447
448 - def get_hidden_categories(self):
449 with self._query() as query: 450 return [ 451 query.get_category(id_) 452 for id_ 453 in self._hidden_category_ids 454 ]
455
456 - def set_hidden_categories(self, hidden_categories):
457 self._hidden_category_ids = [] 458 for cat in hidden_categories: 459 if cat.id not in self._transactions.value.categories: 460 raise ValueError("Category '%s' not in db." % cat.get_name()) 461 self._hidden_category_ids.append(cat.id)
462
463 - def import_db(self, db):
464 if self.get_time_type() != db.get_time_type(): 465 raise Exception("Import failed: time type does not match") 466 with self.transaction("Import events"): 467 for event in db.get_all_events(): 468 self._import_item(event)
469
470 - def _import_item(self, item):
471 if item.is_subevent(): 472 # Sub events are handled when importing the container 473 return 474 new_item = item.duplicate(target_db=self) 475 new_item.category = self._import_category(item.category) 476 new_item.save() 477 if item.is_container(): 478 for subevent in item.subevents: 479 new_sub = subevent.duplicate(target_db=self) 480 new_sub.category = self._import_category(subevent.category) 481 new_sub.container = new_item 482 new_sub.save()
483
484 - def _import_category(self, category):
485 if category is None: 486 return None 487 elif self._has_category_with_name(category.get_name()): 488 return self.get_category_by_name(category.get_name()) 489 else: 490 new_cat = category.duplicate(target_db=self) 491 new_cat.parent = self._import_category(category.parent) 492 new_cat.save() 493 return new_cat
494
495 - def _has_category_with_name(self, name):
496 for category in self.get_categories(): 497 if category.get_name() == name: 498 return True 499 return False
500
501 - def compress(self):
502 with self.transaction("Compress events"): 503 self._set_events_order_from_rows(self._place_events_on_rows())
504
505 - def _set_events_order_from_rows(self, rows):
506 event_sorter = EventSorter() 507 for key in sorted(rows.keys()): 508 event_sorter.save_sort_order(rows[key])
509
510 - def _place_events_on_rows(self):
511 rows = collections.defaultdict(lambda: []) 512 for event in self._length_sort(): 513 inx = 0 514 while True: 515 if self._fits_on_row(rows[inx], event): 516 event.r = inx 517 rows[inx].append(event) 518 break 519 inx += 1 520 return rows
521
522 - def _length_sort(self):
523 all_events = self.get_all_events() 524 reordered_events = [ 525 event 526 for event 527 in all_events 528 if not event.is_subevent() and not event.is_milestone() 529 ] 530 reordered_events = self._sort_by_length(reordered_events) 531 return reordered_events
532
533 - def _sort_by_length(self, events):
534 return sorted(events, key=self._event_length, reverse=True)
535
536 - def _event_length(self, evt):
537 return evt.get_time_period().delta()
538
539 - def _fits_on_row(self, row_events, event):
540 for ev in row_events: 541 if ev.overlaps(event): 542 return False 543 return True
544 545 @contextlib.contextmanager
546 - def _query(self):
547 need_to_create_query = self._current_query is None 548 if need_to_create_query: 549 self._current_query = Query(self, self._transactions.value) 550 try: 551 yield self._current_query 552 finally: 553 if need_to_create_query: 554 self._current_query = None
555
556 557 -class Query(object):
558
559 - def __init__(self, db, immutable_db):
560 self._db = db 561 self._immutable_db = immutable_db 562 self._wrappers = {}
563
564 - def container_exists(self, id_):
565 return id_ in self._immutable_db.containers
566
567 - def event_exists(self, id_):
568 return id_ in self._immutable_db.events
569
570 - def milestone_exists(self, id_):
571 return id_ in self._immutable_db.milestones
572
573 - def get_category(self, id_):
574 if id_ not in self._wrappers: 575 self._wrappers[id_] = self._create_category_wrapper(id_) 576 return self._wrappers[id_]
577
578 - def get_container(self, id_):
579 if id_ not in self._wrappers: 580 self._wrappers[id_] = self._create_container_wrapper(id_) 581 self._load_subevents(self._wrappers[id_]) 582 return self._wrappers[id_]
583
584 - def get_event(self, id_):
585 if id_ not in self._wrappers: 586 self._wrappers[id_] = self._create_event_wrapper(id_) 587 immutable_event = self._immutable_db.events.get(id_) 588 if immutable_event.container_id is not None: 589 # Loading the container will load and populate all subevents 590 self.get_container(immutable_event.container_id) 591 return self._wrappers[id_]
592
593 - def get_milestone(self, id_):
594 if id_ not in self._wrappers: 595 self._wrappers[id_] = self._create_milestone_wrapper(id_) 596 return self._wrappers[id_]
597
598 - def get_era(self, id_):
599 if id_ not in self._wrappers: 600 self._wrappers[id_] = self._create_era_wrapper(id_) 601 return self._wrappers[id_]
602
603 - def _load_subevents(self, container):
604 for subevent_id, immutable_event in self._immutable_db.events: 605 if immutable_event.container_id == container.id: 606 self.get_event(subevent_id).container = container
607
608 - def _create_category_wrapper(self, id_):
609 immutable_category = self._immutable_db.categories.get(id_) 610 wrapper = Category( 611 db=self._db, 612 id_=id_, 613 immutable_value=immutable_category, 614 ) 615 wrapper.parent = self._get_maybe_category(immutable_category.parent_id) 616 return wrapper
617
618 - def _create_container_wrapper(self, id_):
619 immutable_container = self._immutable_db.containers.get(id_) 620 wrapper = Container( 621 db=self._db, 622 id_=id_, 623 immutable_value=immutable_container, 624 ) 625 wrapper.category = self._get_maybe_category(immutable_container.category_id) 626 lst = [] 627 for key in immutable_container.category_ids: 628 lst.append(self._get_maybe_category(key)) 629 wrapper.set_categories(lst) 630 return wrapper
631
632 - def _create_event_wrapper(self, id_):
633 immutable_event = self._immutable_db.events.get(id_) 634 if immutable_event.container_id is None: 635 klass = Event 636 else: 637 klass = Subevent 638 wrapper = klass( 639 db=self._db, 640 id_=id_, 641 immutable_value=immutable_event, 642 ) 643 wrapper.category = self._get_maybe_category(immutable_event.category_id) 644 lst = [] 645 for key in immutable_event.category_ids: 646 lst.append(self._get_maybe_category(key)) 647 wrapper.set_categories(lst) 648 return wrapper
649
650 - def _create_milestone_wrapper(self, id_):
651 immutable_milestone = self._immutable_db.milestones.get(id_) 652 wrapper = Milestone( 653 db=self._db, 654 id_=id_, 655 immutable_value=immutable_milestone, 656 ) 657 wrapper.category = self._get_maybe_category(immutable_milestone.category_id) 658 lst = [] 659 for key in immutable_milestone.category_ids: 660 lst.append(self._get_maybe_category(key)) 661 wrapper.set_categories(lst) 662 return wrapper
663
664 - def _create_era_wrapper(self, id_):
665 immutable_era = self._immutable_db.eras.get(id_) 666 return Era( 667 db=self._db, 668 id_=id_, 669 immutable_value=immutable_era, 670 )
671
672 - def _get_maybe_category(self, category_id):
673 if category_id is None: 674 return None 675 else: 676 return self.get_category(category_id)
677
678 679 -class EventSorter(object):
680
681 - def __init__(self):
682 self._sort_order = 0
683
684 - def save_sort_order(self, events):
685 for event in events: 686 if event.is_container(): 687 self.save_sort_order(event.subevents) 688 else: 689 if event.sort_order != self._sort_order: 690 event.sort_order = self._sort_order 691 event.save() 692 self._sort_order += 1
693
694 695 -def sort_events(events):
696 return sorted(events, key=lambda event: event.sort_order)
697
698 699 -def _generic_event_search(events, search_string):
700 def match(event): 701 target = search_string.lower() 702 description = event.get_data("description") 703 if description is None: 704 description = "" 705 else: 706 description = description.lower() 707 return target in event.get_text().lower() or target in description
708 def mean_time(event): 709 return event.mean_time() 710 matches = [event for event in events if match(event)] 711 matches.sort(key=mean_time) 712 return matches 713