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.iteritems():
     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.set_timeline(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                      immutable_event.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_event):
    199                      immutable_event.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              return [self.find_event_with_id(id_) for id_ in ids]
    414
    415 -    def find_event_with_id(self, event_id):
    416          with self._query() as query:
    417              if query.event_exists(event_id):
    418                  return query.get_event(event_id)
    419              if query.container_exists(event_id):
    420                  return query.get_container(event_id)
    421              if query.milestone_exists(event_id):
    422                  return query.get_milestone(event_id)
    423
    424 -    def _transaction_committed(self):
    425          self._save()
    426          self._notify(STATE_CHANGE_ANY)
    427
    428 -    def _save(self):
    429          if self._save_callback is not None:
    430              self._save_callback()
    431
    432 -    def get_displayed_period(self):
    433          """
    434          Inheritors can call this method to get the displayed period used in
    435          load_view_properties and save_view_properties.
    436          """
    437          return self.displayed_period
    438
    439 -    def set_displayed_period(self, period):
    440          """
    441          Inheritors can call this method to set the displayed period used in
    442          load_view_properties and save_view_properties.
    443          """
    444          self.displayed_period = period
    445
    446 -    def get_hidden_categories(self):
    447          with self._query() as query:
    448              return [
    449                  query.get_category(id_)
    450                  for id_
    451                  in self._hidden_category_ids
    452              ]
    453
    454 -    def set_hidden_categories(self, hidden_categories):
    455          self._hidden_category_ids = []
    456          for cat in hidden_categories:
    457              if cat.id not in self._transactions.value.categories:
    458                  raise ValueError("Category '%s' not in db." % cat.get_name())
    459              self._hidden_category_ids.append(cat.id)
    460
    461 -    def import_db(self, db):
    462          if self.get_time_type() != db.get_time_type():
    463              raise Exception("Import failed: time type does not match")
    464          with self.transaction("Import events"):
    465              for event in db.get_all_events():
    466                  self._import_item(event)
    467
    468 -    def _import_item(self, item):
    469          if item.is_subevent():
    470              # Sub events are handled when importing the container
    471              return
    472          new_item = item.duplicate(target_db=self)
    473          new_item.category = self._import_category(item.category)
    474          new_item.save()
    475          if item.is_container():
    476              for subevent in item.subevents:
    477                  new_sub = subevent.duplicate(target_db=self)
    478                  new_sub.category = self._import_category(subevent.category)
    479                  new_sub.container = new_item
    480                  new_sub.save()
    481
    482 -    def _import_category(self, category):
    483          if category is None:
    484              return None
    485          elif self._has_category_with_name(category.get_name()):
    486              return self.get_category_by_name(category.get_name())
    487          else:
    488              new_cat = category.duplicate(target_db=self)
    489              new_cat.parent = self._import_category(category.parent)
    490              new_cat.save()
    491              return new_cat
    492
    493 -    def _has_category_with_name(self, name):
    494          for category in self.get_categories():
    495              if category.get_name() == name:
    496                  return True
    497          return False
    498
    499 -    def compress(self):
    500          with self.transaction("Compress events"):
    501              self._set_events_order_from_rows(self._place_events_on_rows())
    502
    503 -    def _set_events_order_from_rows(self, rows):
    504          event_sorter = EventSorter()
    505          for key in sorted(rows.keys()):
    506              event_sorter.save_sort_order(rows[key])
    507
    508 -    def _place_events_on_rows(self):
    509          rows = collections.defaultdict(lambda: [])
    510          for event in self._length_sort():
    511              inx = 0
    512              while True:
    513                  if self._fits_on_row(rows[inx], event):
    514                      event.r = inx
    515                      rows[inx].append(event)
    516                      break
    517                  inx += 1
    518          return rows
    519
    520 -    def _length_sort(self):
    521          all_events = self.get_all_events()
    522          reordered_events = [
    523              event
    524              for event
    525              in all_events
    526              if not event.is_subevent()
    527          ]
    528          reordered_events = self._sort_by_length(reordered_events)
    529          return reordered_events
    530
    531 -    def _sort_by_length(self, events):
    532          return sorted(events, key=self._event_length, reverse=True)
    533
    534 -    def _event_length(self, evt):
    535          return evt.get_time_period().delta()
    536
    537 -    def _fits_on_row(self, row_events, event):
    538          for ev in row_events:
    539              if ev.overlaps(event):
    540                  return False
    541          return True
    542
    543      @contextlib.contextmanager
    544 -    def _query(self):
    545          need_to_create_query = self._current_query is None
    546          if need_to_create_query:
    547              self._current_query = Query(self, self._transactions.value)
    548          try:
    549              yield self._current_query
    550          finally:
    551              if need_to_create_query:
    552                  self._current_query = None
    553
    554
    555 -class Query(object):
    556
    557 -    def __init__(self, db, immutable_db):
    558          self._db = db
    559          self._immutable_db = immutable_db
    560          self._wrappers = {}
    561
    562 -    def container_exists(self, id_):
    563          return id_ in self._immutable_db.containers
    564
    565 -    def event_exists(self, id_):
    566          return id_ in self._immutable_db.events
    567
    568 -    def milestone_exists(self, id_):
    569          return id_ in self._immutable_db.milestones
    570
    571 -    def get_category(self, id_):
    572          if id_ not in self._wrappers:
    573              self._wrappers[id_] = self._create_category_wrapper(id_)
    574          return self._wrappers[id_]
    575
    576 -    def get_container(self, id_):
    577          if id_ not in self._wrappers:
    578              self._wrappers[id_] = self._create_container_wrapper(id_)
    579              self._load_subevents(self._wrappers[id_])
    580          return self._wrappers[id_]
    581
    582 -    def get_event(self, id_):
    583          if id_ not in self._wrappers:
    584              self._wrappers[id_] = self._create_event_wrapper(id_)
    585              immutable_event = self._immutable_db.events.get(id_)
    586              if immutable_event.container_id is not None:
    587                  # Loading the container will load and populate all subevents
    588                  self.get_container(immutable_event.container_id)
    589          return self._wrappers[id_]
    590
    591 -    def get_milestone(self, id_):
    592          if id_ not in self._wrappers:
    593              self._wrappers[id_] = self._create_milestone_wrapper(id_)
    594          return self._wrappers[id_]
    595
    596 -    def get_era(self, id_):
    597          if id_ not in self._wrappers:
    598              self._wrappers[id_] = self._create_era_wrapper(id_)
    599          return self._wrappers[id_]
    600
    601 -    def _load_subevents(self, container):
    602          for subevent_id, immutable_event in self._immutable_db.events:
    603              if immutable_event.container_id == container.id:
    604                  self.get_event(subevent_id).container = container
    605
    606 -    def _create_category_wrapper(self, id_):
    607          immutable_category = self._immutable_db.categories.get(id_)
    608          wrapper = Category(
    609              db=self._db,
    610              id_=id_,
    611              immutable_value=immutable_category,
    612          )
    613          wrapper.parent = self._get_maybe_category(immutable_category.parent_id)
    614          return wrapper
    615
    616 -    def _create_container_wrapper(self, id_):
    617          immutable_container = self._immutable_db.containers.get(id_)
    618          wrapper = Container(
    619              db=self._db,
    620              id_=id_,
    621              immutable_value=immutable_container,
    622          )
    623          wrapper.category = self._get_maybe_category(immutable_container.category_id)
    624          return wrapper
    625
    626 -    def _create_event_wrapper(self, id_):
    627          immutable_event = self._immutable_db.events.get(id_)
    628          if immutable_event.container_id is None:
    629              klass = Event
    630          else:
    631              klass = Subevent
    632          wrapper = klass(
    633              db=self._db,
    634              id_=id_,
    635              immutable_value=immutable_event,
    636          )
    637          wrapper.category = self._get_maybe_category(immutable_event.category_id)
    638          return wrapper
    639
    640 -    def _create_milestone_wrapper(self, id_):
    641          immutable_milestone = self._immutable_db.milestones.get(id_)
    642          wrapper = Milestone(
    643              db=self._db,
    644              id_=id_,
    645              immutable_value=immutable_milestone,
    646          )
    647          wrapper.category = self._get_maybe_category(immutable_milestone.category_id)
    648          return wrapper
    649
    650 -    def _create_era_wrapper(self, id_):
    651          immutable_era = self._immutable_db.eras.get(id_)
    652          return Era(
    653              db=self._db,
    654              id_=id_,
    655              immutable_value=immutable_era,
    656          )
    657
    658 -    def _get_maybe_category(self, category_id):
    659          if category_id is None:
    660              return None
    661          else:
    662              return self.get_category(category_id)
    663
    664
    665 -class EventSorter(object):
    666
    667 -    def __init__(self):
    668          self._sort_order = 0
    669
    670 -    def save_sort_order(self, events):
    671          for event in events:
    672              if event.is_container():
    673                  self.save_sort_order(event.subevents)
    674              else:
    675                  if event.sort_order != self._sort_order:
    676                      event.sort_order = self._sort_order
    677                      event.save()
    678                  self._sort_order += 1
    679
    680
    681 -def sort_events(events):
    682      return sorted(events, key=lambda event: event.sort_order)
    683
    684
    685 -def _generic_event_search(events, search_string):
    686      def match(event):
    687          target = search_string.lower()
    688          description = event.get_data("description")
    689          if description is None:
    690              description = ""
    691          else:
    692              description = description.lower()
    693          return target in event.get_text().lower() or target in description
    694      def mean_time(event):
    695          return event.mean_time()
    696      matches = [event for event in events if match(event)]
    697      matches.sort(key=mean_time)
    698      return matches
    699