~bertrand-rousseau/gtg/new-task-list-layout

« back to all changes in this revision

Viewing changes to GTG/core/datastore.py

  • Committer: Bertrand Rousseau
  • Date: 2012-07-13 17:24:28 UTC
  • mfrom: (1178.1.28 trunk)
  • mto: (1178.1.30 trunk)
  • mto: This revision was merged to the branch mainline in revision 1183.
  • Revision ID: bertrand.rousseau@gmail.com-20120713172428-ou3ic646fccov41d
Merge with trunk. Fixes conflict with CHANGELOG.

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
import os.path
31
31
from collections import deque
32
32
 
33
 
from GTG.core                    import requester
34
 
from GTG.core.task               import Task
35
 
from GTG.core.tag                import Tag
36
 
from GTG.core                    import CoreConfig
37
 
from GTG.core.treefactory        import TreeFactory
38
 
from GTG.tools.logger            import Log
 
33
from GTG.core import requester
 
34
from GTG.core.task import Task
 
35
from GTG.core.tag import Tag
 
36
from GTG.core import CoreConfig
 
37
from GTG.core.treefactory import TreeFactory
 
38
from GTG.tools.logger import Log
39
39
from GTG.backends.genericbackend import GenericBackend
40
 
from GTG.tools                   import cleanxml
 
40
from GTG.tools import cleanxml
41
41
from GTG.backends.backendsignals import BackendSignals
42
 
from GTG.tools.borg              import Borg
43
 
from GTG.core.search             import parse_search_query, search_filter, InvalidQuery
 
42
from GTG.tools.borg import Borg
 
43
from GTG.core.search import parse_search_query, search_filter, InvalidQuery
44
44
 
45
45
 
46
46
class DataStore(object):
47
 
    '''
 
47
    """
48
48
    A wrapper around all backends that is responsible for keeping the backend
49
49
    instances. It can enable, disable, register and destroy backends, and acts
50
50
    as interface between the backends and GTG core.
51
51
    You should not interface yourself directly with the DataStore: use the
52
52
    Requester instead (which also sends signals as you issue commands).
53
 
    '''
 
53
    """
54
54
 
55
55
    def __init__(self, global_conf=CoreConfig()):
56
 
        '''
 
56
        """
57
57
        Initializes a DataStore object
58
 
        '''
 
58
        """
59
59
        self.backends = {} #dictionary {backend_name_string: Backend instance}
60
60
        self.treefactory = TreeFactory()
61
61
        self.__tasks = self.treefactory.get_tasks_tree()
79
79
    ##########################################################################
80
80
    ### Helper functions (get_ methods for Datastore embedded objects)
81
81
    ##########################################################################
82
 
 
83
82
    def get_tagstore(self):
84
 
        '''
 
83
        """
85
84
        Helper function to obtain the Tagstore associated with this DataStore
86
85
 
87
86
        @return GTG.core.tagstore.TagStore: the tagstore object
88
 
        '''
 
87
        """
89
88
        return self.__tagstore
90
89
 
91
90
    def get_requester(self):
92
 
        '''
 
91
        """
93
92
        Helper function to get the Requester associate with this DataStore
94
93
 
95
94
        @returns GTG.core.requester.Requester: the requester associated with
96
95
                                               this datastore
97
 
        '''
 
96
        """
98
97
        return self.requester
99
98
 
100
99
    def get_tasks_tree(self):
101
 
        '''
 
100
        """
102
101
        Helper function to get a Tree with all the tasks contained in this
103
102
        Datastore.
104
103
 
105
104
        @returns GTG.core.tree.Tree: a task tree (the main one)
106
 
        '''
 
105
        """
107
106
        return self.__tasks
108
107
 
109
108
    ##########################################################################
110
109
    ### Tags functions
111
110
    ##########################################################################
112
 
 
113
111
    def _add_new_tag(self, name, tag, filter_func, parameters, parent_id=None):
114
112
        """ Add tag into a tree """
115
113
        name = name.encode("UTF-8")
155
153
            parameters, parent_id = CoreConfig.SEARCH_TAG)
156
154
        Log.debug("*** view added %s ***" % name)
157
155
        return tag
158
 
    
 
156
 
159
157
    def remove_tag(self, name):
160
158
        """ Removes a tag from the tagtree """
161
159
        if self.__tagstore.has_node(name):
173
171
        NOTE: Implementation for regular tasks must be much more robust.
174
172
        You have to replace all occurences of tag name in tasks descriptions,
175
173
        their parameters and backend settings (synchronize only certain tags).
176
 
        
 
174
 
177
175
        Have a fun with implementing it!
178
176
        """
179
177
        tag = self.get_tag(oldname)
244
242
 
245
243
        doc, xmlroot = cleanxml.emptydoc(TAG_XMLROOT)
246
244
        tags = self.__tagstore.get_main_view().get_all_nodes()
247
 
        already_saved = [] 
248
 
        
 
245
        already_saved = []
 
246
 
249
247
        for tagname in tags:
250
248
            if tagname in already_saved:
251
249
                continue
268
266
 
269
267
            xmlroot.appendChild(t_xml)
270
268
            already_saved.append(tagname)
271
 
                    
 
269
 
272
270
        cleanxml.savexml(self.tagfile, doc)
273
 
    
 
271
 
274
272
    ##########################################################################
275
273
    ### Tasks functions
276
274
    ##########################################################################
277
 
 
278
275
    def get_all_tasks(self):
279
 
        '''
 
276
        """
280
277
        Returns list of all keys of open tasks
281
278
 
282
279
        @return a list of strings: a list of task ids
283
 
        '''
 
280
        """
284
281
        return self.__tasks.get_main_view().get_all_nodes()
285
282
 
286
283
    def has_task(self, tid):
287
 
        '''
 
284
        """
288
285
        Returns true if the tid is among the open or closed tasks for
289
286
        this DataStore, False otherwise.
290
287
 
291
288
        @param tid: Task ID to search for
292
289
        @return bool: True if the task is present
293
 
        '''
 
290
        """
294
291
        return self.__tasks.has_node(tid)
295
292
 
296
293
    def get_task(self, tid):
297
 
        '''
 
294
        """
298
295
        Returns the internal task object for the given tid, or None if the
299
296
        tid is not present in this DataStore.
300
297
 
301
298
        @param tid: Task ID to retrieve
302
299
        @returns GTG.core.task.Task or None:  whether the Task is present
303
300
        or not
304
 
        '''
 
301
        """
305
302
        if self.has_task(tid):
306
303
            return self.__tasks.get_node(tid)
307
304
        else:
311
308
            return None
312
309
 
313
310
    def task_factory(self, tid, newtask=False):
314
 
        '''
 
311
        """
315
312
        Instantiates the given task id as a Task object.
316
313
 
317
314
        @param tid: a task id. Must be unique
318
315
        @param newtask: True if the task has never been seen before
319
316
        @return Task: a Task instance
320
 
        '''
 
317
        """
321
318
        return Task(tid, self.requester, newtask)
322
319
 
323
320
    def new_task(self):
334
331
        return task
335
332
 
336
333
    def push_task(self, task):
337
 
        '''
 
334
        """
338
335
        Adds the given task object to the task tree. In other words, registers
339
336
        the given task in the GTG task set.
340
337
        This function is used in mutual exclusion: only a backend at a time is
342
339
 
343
340
        @param task: A valid task object  (a GTG.core.task.Task)
344
341
        @return bool: True if the task has been accepted
345
 
        '''
 
342
        """
 
343
 
346
344
        def adding(task):
347
345
            priority = task.get_update_priority()
348
346
            self.__tasks.add_node(task, priority=priority)
359
357
    ##########################################################################
360
358
    ### Backends functions
361
359
    ##########################################################################
362
 
 
363
360
    def get_all_backends(self, disabled=False):
364
361
        """
365
362
        returns list of all registered backends for this DataStore.
366
363
 
367
 
        @param disabled: If disabled is True, attaches also the list of disabled backends
 
364
        @param disabled: If disabled is True, attaches also the list of
 
365
                disabled backends
368
366
        @return list: a list of TaskSource objects
369
367
        """
370
368
        result = []
374
372
        return result
375
373
 
376
374
    def get_backend(self, backend_id):
377
 
        '''
 
375
        """
378
376
        Returns a backend given its id.
379
377
 
380
378
        @param backend_id: a backend id
381
379
        @returns GTG.core.datastore.TaskSource or None: the requested backend,
382
380
                                                        or None
383
 
        '''
 
381
        """
384
382
        if backend_id in self.backends:
385
383
            return self.backends[backend_id]
386
384
        else:
433
431
            Log.error("Tried to register a backend without a  pid")
434
432
 
435
433
    def _activate_non_default_backends(self, sender=None):
436
 
        '''
 
434
        """
437
435
        Non-default backends have to wait until the default loads before
438
436
        being  activated. This function is called after the first default
439
437
        backend has loaded all its tasks.
440
438
 
441
439
        @param sender: not used, just here for signal compatibility
442
 
        '''
 
440
        """
443
441
        if self.is_default_backend_loaded:
444
442
            Log.debug("spurious call")
445
443
            return
450
448
                self._backend_startup(backend)
451
449
 
452
450
    def _backend_startup(self, backend):
453
 
        '''
 
451
        """
454
452
        Helper function to launch a thread that starts a backend.
455
453
 
456
454
        @param backend: the backend object
457
 
        '''
 
455
        """
 
456
 
458
457
        def __backend_startup(self, backend):
459
 
            '''
 
458
            """
460
459
            Helper function to start a backend
461
460
 
462
461
            @param backend: the backend object
463
 
            '''
 
462
            """
464
463
            backend.initialize()
465
464
            backend.start_get_tasks()
466
465
            self.flush_all_tasks(backend.get_id())
501
500
                                       True)
502
501
 
503
502
    def remove_backend(self, backend_id):
504
 
        '''
 
503
        """
505
504
        Removes a backend, and forgets it ever existed.
506
505
 
507
506
        @param backend_id: a backend id
508
 
        '''
 
507
        """
509
508
        if backend_id in self.backends:
510
509
            backend = self.backends[backend_id]
511
510
            if backend.is_enabled():
512
511
                self.set_backend_enabled(backend_id, False)
513
512
            #FIXME: to keep things simple, backends are not notified that they
514
513
            #       are completely removed (they think they're just
515
 
            #       deactivated). We should add a "purge" call to backend to let
516
 
            #       them know that they're removed, so that they can remove all
517
 
            #       the various files they've created. (invernizzi)
 
514
            #       deactivated). We should add a "purge" call to backend to
 
515
            #       let them know that they're removed, so that they can
 
516
            #       remove all the various files they've created. (invernizzi)
518
517
 
519
518
            #we notify that the backend has been deleted
520
519
            self._backend_signals.backend_removed(backend.get_id())
521
520
            del self.backends[backend_id]
522
521
 
523
522
    def backend_change_attached_tags(self, backend_id, tag_names):
524
 
        '''
 
523
        """
525
524
        Changes the tags for which a backend should store a task
526
525
 
527
526
        @param backend_id: a backend_id
528
527
        @param tag_names: the new set of tags. This should not be a tag object,
529
528
                          just the tag name.
530
 
        '''
 
529
        """
531
530
        backend = self.backends[backend_id]
532
531
        backend.set_attached_tags(tag_names)
533
532
 
534
533
    def flush_all_tasks(self, backend_id):
535
 
        '''
 
534
        """
536
535
        This function will cause all tasks to be checked against the backend
537
536
        identified with backend_id. If tasks need to be added or removed, it
538
537
        will be done here.
541
540
        the Tree will be saved in the proper backends
542
541
 
543
542
        @param backend_id: a backend id
544
 
        '''
 
543
        """
 
544
 
545
545
        def _internal_flush_all_tasks():
546
546
            backend = self.backends[backend_id]
547
547
            for task_id in self.get_all_tasks():
553
553
        self.backends[backend_id].start_get_tasks()
554
554
 
555
555
    def save(self, quit=False):
556
 
        '''
 
556
        """
557
557
        Saves the backends parameters.
558
558
 
559
559
        @param quit: If quit is true, backends are shut down
560
 
        '''
 
560
        """
561
561
        try:
562
562
            self.start_get_tasks_thread.join()
563
563
        except Exception:
590
590
                t_xml.setAttribute(str(key), value)
591
591
            #Saving all the projects at close
592
592
            xmlconfig.appendChild(t_xml)
593
 
        datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
 
593
        datadir = CoreConfig().get_data_dir()
 
594
        datafile = os.path.join(datadir, CoreConfig.DATA_FILE)
594
595
        cleanxml.savexml(datafile, doc, backup=True)
595
596
        #Saving the tagstore
596
597
        self.save_tagtree()
597
598
 
598
599
    def request_task_deletion(self, tid):
599
 
        '''
 
600
        """
600
601
        This is a proxy function to request a task deletion from a backend
601
602
 
602
603
        @param tid: the tid of the task to remove
603
 
        '''
 
604
        """
604
605
        self.requester.delete_task(tid)
605
606
 
606
607
    def get_backend_mutex(self):
607
 
        '''
 
608
        """
608
609
        Returns the mutex object used by backends to avoid modifying a task
609
610
        at the same time.
610
611
 
611
612
        @returns: threading.Lock
612
 
        '''
 
613
        """
613
614
        return self._backend_mutex
614
615
 
615
616
 
616
617
class TaskSource():
617
 
    '''
 
618
    """
618
619
    Transparent interface between the real backend and the DataStore.
619
620
    Is in charge of connecting and disconnecting to signals
620
 
    '''
 
621
    """
621
622
 
622
623
    def __init__(self, requester, backend, datastore):
623
624
        """
653
654
            BackendSignals().default_backend_loaded()
654
655
 
655
656
    def get_task_filter_for_backend(self):
656
 
        '''
 
657
        """
657
658
        Filter that checks if the task should be stored in this backend.
658
659
 
659
 
        @returns function: a function that accepts a task and returns True/False
660
 
                 whether the task should be stored or not
661
 
        '''
 
660
        @returns function: a function that accepts a task and returns
 
661
                 True/False whether the task should be stored or not
 
662
        """
 
663
 
662
664
        def backend_filter(req, task, parameters):
663
 
            '''
664
 
            Filter that checks if two tags sets intersect. It is used to check if a
665
 
            task should be stored inside a backend
 
665
            """
 
666
            Filter that checks if two tags sets intersect. It is used to check
 
667
            if a task should be stored inside a backend
666
668
            @param task: a task object
667
 
            @oaram tags_to_match_set: a *set* of tag names
668
 
            '''
 
669
            @param tags_to_match_set: a *set* of tag names
 
670
            """
669
671
            try:
670
672
                tags_to_match_set = parameters['tags']
671
673
            except KeyError:
680
682
                        {"tags": set(self.backend.get_attached_tags())})
681
683
 
682
684
    def should_task_id_be_stored(self, task_id):
683
 
        '''
 
685
        """
684
686
        Helper function:  Checks if a task should be stored in this backend
685
687
 
686
688
        @param task_id: a task id
687
689
        @returns bool: True if the task should be stored
688
 
        '''
 
690
        """
689
691
        #task = self.req.get_task(task_id)
690
692
        #FIXME: it will be a lot easier to add, instead,
691
693
        # a filter to a tree and check that this task is well in the tree
708
710
            self.queue_remove_task(tid, path)
709
711
 
710
712
    def launch_setting_thread(self, bypass_please_quit=False):
711
 
        '''
 
713
        """
712
714
        Operates the threads to set and remove tasks.
713
715
        Releases the lock when it is done.
714
716
 
715
 
        @param bypass_please_quit: if True, the self.please_quit "quit condition"
716
 
                                   is ignored. Currently, it's turned to true
717
 
                                   after the quit condition has been issued, to
718
 
                                   execute eventual pending operations.
719
 
        '''
 
717
        @param bypass_please_quit: if True, the self.please_quit
 
718
                                   "quit condition" is ignored. Currently,
 
719
                                   it's turned to true after the quit
 
720
                                   condition has been issued, to execute
 
721
                                   eventual pending operations.
 
722
        """
720
723
        while not self.please_quit or bypass_please_quit:
721
724
            try:
722
725
                tid = self.to_set.pop()
740
743
        self.to_set_timer = None
741
744
 
742
745
    def queue_remove_task(self, tid, path=None):
743
 
        '''
 
746
        """
744
747
        Queues task to be removed.
745
748
 
746
749
        @param sender: not used, any value will do
747
750
        @param tid: The Task ID of the task to be removed
748
 
        '''
 
751
        """
749
752
        if tid not in self.to_remove:
750
753
            self.to_remove.appendleft(tid)
751
754
            self.__try_launch_setting_thread()
752
755
 
753
756
    def __try_launch_setting_thread(self):
754
 
        '''
 
757
        """
755
758
        Helper function to launch the setting thread, if it's not running
756
 
        '''
 
759
        """
757
760
        if self.to_set_timer == None and not self.please_quit:
758
761
            self.to_set_timer = threading.Timer(self.timer_timestep, \
759
762
                                        self.launch_setting_thread)
761
764
            self.to_set_timer.start()
762
765
 
763
766
    def initialize(self, connect_signals=True):
764
 
        '''
 
767
        """
765
768
        Initializes the backend and starts looking for signals.
766
769
 
767
770
        @param connect_signals: if True, it starts listening for signals
768
 
        '''
 
771
        """
769
772
        self.backend.initialize()
770
773
        if connect_signals:
771
774
            self._connect_signals()
772
775
 
773
776
    def _connect_signals(self):
774
 
        '''
 
777
        """
775
778
        Helper function to connect signals
776
 
        '''
 
779
        """
777
780
        if not self.add_task_handle:
778
 
            self.add_task_handle = self.tasktree.register_cllbck('node-added', \
779
 
                                                    self.queue_set_task)
 
781
            self.add_task_handle = self.tasktree.register_cllbck(
 
782
                'node-added', self.queue_set_task)
780
783
        if not self.set_task_handle:
781
 
            self.set_task_handle = self.tasktree.register_cllbck('node-modified', \
782
 
                                                    self.queue_set_task)
 
784
            self.set_task_handle = self.tasktree.register_cllbck(
 
785
                'node-modified', self.queue_set_task)
783
786
        if not self.remove_task_handle:
784
 
            self.remove_task_handle = self.tasktree.register_cllbck('node-deleted',\
785
 
                                                   self.queue_remove_task)
 
787
            self.remove_task_handle = self.tasktree.register_cllbck(
 
788
                'node-deleted', self.queue_remove_task)
786
789
 
787
790
    def _disconnect_signals(self):
788
 
        '''
 
791
        """
789
792
        Helper function to disconnect signals
790
 
        '''
 
793
        """
791
794
        if self.add_task_handle:
792
 
            self.tasktree.deregister_cllbck('node-added', self.set_task_handle)
 
795
            self.tasktree.deregister_cllbck('node-added',
 
796
                self.set_task_handle)
793
797
            self.add_task_handle = None
794
798
        if self.set_task_handle:
795
 
            self.tasktree.deregister_cllbck('node-modified', self.set_task_handle)
 
799
            self.tasktree.deregister_cllbck('node-modified',
 
800
                self.set_task_handle)
796
801
            self.set_task_handle = None
797
802
        if  self.remove_task_handle:
798
 
            self.tasktree.deregister_cllbck('node-deleted', self.remove_task_handle)
 
803
            self.tasktree.deregister_cllbck('node-deleted',
 
804
                self.remove_task_handle)
799
805
            self.remove_task_handle = None
800
806
 
801
807
    def sync(self):
802
 
        '''
 
808
        """
803
809
        Forces the TaskSource to sync all the pending tasks
804
 
        '''
 
810
        """
805
811
        try:
806
812
            self.to_set_timer.cancel()
807
813
        except Exception:
817
823
        self.launch_setting_thread(bypass_please_quit=True)
818
824
 
819
825
    def quit(self, disable=False):
820
 
        '''
 
826
        """
821
827
        Quits the backend and disconnect the signals
822
828
 
823
829
        @param disable: if True, the backend is disabled.
824
 
        '''
 
830
        """
825
831
        self._disconnect_signals()
826
832
        self.please_quit = True
827
833
        self.sync()
828
834
        self.backend.quit(disable)
829
835
 
830
836
    def __getattr__(self, attr):
831
 
        '''
 
837
        """
832
838
        Delegates all the functions not defined here to the real backend
833
839
        (standard python function)
834
840
 
835
841
        @param attr: attribute to get
836
 
        '''
 
842
        """
837
843
        if attr in self.__dict__:
838
844
            return self.__dict__[attr]
839
845
        else:
841
847
 
842
848
 
843
849
class FilteredDataStore(Borg):
844
 
    '''
 
850
    """
845
851
    This class acts as an interface to the Datastore.
846
852
    It is used to hide most of the methods of the Datastore.
847
853
    The backends can safely use the remaining methods.
848
 
    '''
 
854
    """
849
855
 
850
856
    def __init__(self, datastore):
851
857
        super(FilteredDataStore, self).__init__()