Gnumed.timelinelib.canvas.data.db

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