1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4
# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
6
# This program is free software: you can redistribute it and/or modify it under
7
# the terms of the GNU General Public License as published by the Free Software
8
# Foundation, either version 3 of the License, or (at your option) any later
11
# This program is distributed in the hope that it will be useful, but WITHOUT
12
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16
# You should have received a copy of the GNU General Public License along with
17
# this program. If not, see <http://www.gnu.org/licenses/>.
18
# -----------------------------------------------------------------------------
29
from collections import deque
31
from GTG.backends.backendsignals import BackendSignals
32
from GTG.tools.keyring import Keyring
33
from GTG.core import CoreConfig
34
from GTG.tools.logger import Log
39
class GenericBackend(object):
41
Base class for every backend. It's a little more than an interface which
42
methods have to be redefined in order for the backend to run.
46
#BACKEND TYPE DESCRIPTION
47
#"_general_description" is a dictionary that holds the values for the
49
BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
50
# translations, *must be unique*)
51
BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
52
BACKEND_DESCRIPTION = "description" #A short description of the backend
53
BACKEND_AUTHORS = "authors" #a list of strings
55
#BACKEND_TYPE is one of:
56
TYPE_READWRITE = "readwrite"
57
TYPE_READONLY = "readonly"
58
TYPE_IMPORT = "import"
59
TYPE_EXPORT = "export"
60
_general_description = {}
63
#"static_parameters" is a dictionary of dictionaries, each of which
64
#representing a parameter needed to configure the backend.
65
#each "sub-dictionary" is identified by this a key representing its name.
66
#"static_parameters" will be part of the definition of each
68
# Each dictionary contains the keys:
69
#PARAM_DESCRIPTION = "description" #short description (shown to the user
70
# during configuration)
71
PARAM_DEFAULT_VALUE = "default_value" # its default value
73
#PARAM_TYPE is one of the following (changing this changes the way
74
# the user can configure the parameter)
75
TYPE_PASSWORD = "password" #the real password is stored in the GNOME
77
# This is just a key to find it there
78
TYPE_STRING = "string" #generic string, nothing fancy is done
79
TYPE_INT = "int" #edit box can contain only integers
80
TYPE_BOOL = "bool" #checkbox is shown
81
TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
82
# prohibited in strings
83
_static_parameters = {}
87
Called each time it is enabled again (including on backend creation).
88
Please note that a class instance for each disabled backend *is*
89
created, but it's not initialized.
91
NOTE: make sure to call super().initialize()
93
for module_name in self.get_required_modules():
94
sys.modules[module_name]= __import__(module_name)
95
self._parameters[self.KEY_ENABLED] = True
96
self._is_initialized = True
97
#we signal that the backend has been enabled
98
self._signal_manager.backend_state_changed(self.get_id())
100
def start_get_tasks(self):
102
Once this function is launched, the backend can start pushing
103
tasks to gtg parameters.
105
@return: start_get_tasks() might not return or finish
107
raise NotImplemented()
109
def set_task(self, task):
111
Save the task in the backend. If the task id is new for the
112
backend, then a new task must be created.
116
def remove_task(self, tid):
117
''' Completely remove the task with ID = tid '''
120
def has_task(self, tid):
121
'''Returns true if the backend has an internal idea
122
of the task corresponding to the tid. False otherwise'''
123
raise NotImplemented()
125
def new_task_id(self):
127
Returns an available ID for a new task so that a task with this ID
128
can be saved with set_task later.
130
raise NotImplemented()
132
def this_is_the_first_run(self, xml):
134
Steps to execute if it's the first time the backend is run. Optional.
140
Called when a backend will be removed from GTG. Useful for removing
141
configuration files. Optional.
145
def get_number_of_tasks(self):
147
Returns the number of tasks stored in the backend. Doesn't need to be a
148
fast function, is called just for the UI
150
raise NotImplemented()
153
def get_required_modules():
156
def quit(self, disable = False):
158
Called when GTG quits or disconnects the backend. Remember to execute
159
also this function when quitting. If disable is True, the backend won't
160
be automatically loaded at next GTG start
162
self._is_initialized = False
164
self._parameters[self.KEY_ENABLED] = False
165
#we signal that we have been disabled
166
self._signal_manager.backend_state_changed(self.get_id())
167
self._signal_manager.backend_sync_ended(self.get_id())
168
syncing_thread = threading.Thread(target = self.sync).run()
170
def save_state(self):
172
It's the last function executed on a quitting backend, after the
173
pending actions have been done.
174
Useful to ensure that the state is saved in a consistent manner
178
###############################################################################
179
###### You don't need to reimplement the functions below this line ############
180
###############################################################################
182
#These parameters are common to all backends and necessary.
183
# They will be added automatically to your _static_parameters list
184
#NOTE: for now I'm disabling changing the default backend. Once it's all
185
# set up, we will see about that (invernizzi)
186
KEY_DEFAULT_BACKEND = "Default"
187
KEY_ENABLED = "Enabled"
188
KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
189
KEY_ATTACHED_TAGS = "attached-tags"
192
ALLTASKS_TAG = "gtg-tags-all" #IXME: moved here to avoid circular imports
194
_static_parameters_obligatory = { \
195
KEY_DEFAULT_BACKEND: { \
196
PARAM_TYPE: TYPE_BOOL, \
197
PARAM_DEFAULT_VALUE: False, \
200
PARAM_TYPE: TYPE_STRING, \
201
PARAM_DEFAULT_VALUE: "", \
204
PARAM_TYPE: TYPE_STRING, \
205
PARAM_DEFAULT_VALUE: "", \
208
PARAM_TYPE: TYPE_STRING, \
209
PARAM_DEFAULT_VALUE: "", \
212
PARAM_TYPE: TYPE_BOOL, \
213
PARAM_DEFAULT_VALUE: False, \
216
_static_parameters_obligatory_for_rw = { \
217
KEY_ATTACHED_TAGS: {\
218
PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
219
PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
222
#Handy dictionary used in type conversion (from string to type)
223
_type_converter = {TYPE_STRING: str,
228
def _get_static_parameters(cls):
230
Helper method, used to obtain the full list of the static_parameters
231
(user configured and default ones)
233
if hasattr(cls, "_static_parameters"):
234
temp_dic = cls._static_parameters_obligatory.copy()
235
if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
237
cls._static_parameters_obligatory_for_rw.iteritems():
238
temp_dic[key] = value
239
for key, value in cls._static_parameters.iteritems():
240
temp_dic[key] = value
243
raise NotImplemented("_static_parameters not implemented for " + \
244
"backend %s" % type(cls))
246
def __init__(self, parameters):
248
Instantiates a new backend. Please note that this is called also for
249
disabled backends. Those are not initialized, so you might want to check
250
out the initialize() function.
252
if self.KEY_DEFAULT_BACKEND not in parameters:
253
parameters[self.KEY_DEFAULT_BACKEND] = True
254
if parameters[self.KEY_DEFAULT_BACKEND] or \
255
(not self.KEY_ATTACHED_TAGS in parameters and \
256
self._general_description[self.BACKEND_TYPE] \
257
== self.TYPE_READWRITE):
258
parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
259
self._parameters = parameters
260
self._signal_manager = BackendSignals()
261
self._is_initialized = False
262
if Log.is_debugging_mode():
263
self.timer_timestep = 5
265
self.timer_timestep = 1
266
self.to_set_timer = None
267
self.please_quit = False
268
self.to_set = deque()
269
self.to_remove = deque()
271
def get_attached_tags(self):
273
Returns the list of tags which are handled by this backend
275
if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
276
self._parameters[self.KEY_DEFAULT_BACKEND]:
277
return [self.ALLTASKS_TAG]
279
return self._parameters[self.KEY_ATTACHED_TAGS]
283
def set_attached_tags(self, tags):
285
Changes the set of attached tags
287
self._parameters[self.KEY_ATTACHED_TAGS] = tags
290
def get_static_parameters(cls):
292
Returns a dictionary of parameters necessary to create a backend.
294
return cls._get_static_parameters()
296
def get_parameters(self):
298
Returns a dictionary of the current parameters.
300
return self._parameters
302
def set_parameter(self, parameter, value):
303
self._parameters[parameter] = value
308
Returns the name of the backend as it should be displayed in the UI
310
return cls._get_from_general_description(cls.BACKEND_NAME)
313
def get_description(cls):
314
"""Returns a description of the backend"""
315
return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
319
"""Returns the backend type(readonly, r/w, import, export) """
320
return cls._get_from_general_description(cls.BACKEND_TYPE)
323
def get_authors(cls):
325
returns the backend author(s)
327
return cls._get_from_general_description(cls.BACKEND_AUTHORS)
330
def _get_from_general_description(cls, key):
332
Helper method to extract values from cls._general_description.
333
Raises an exception if the key is missing (helpful for developers
334
adding new backends).
336
if key in cls._general_description:
337
return cls._general_description[key]
339
raise NotImplemented("Key %s is missing from " +\
340
"'self._general_description' of a backend (%s). " +
341
"Please add the corresponding value" % (key, type(cls)))
344
def cast_param_type_from_string(cls, param_value, param_type):
346
Parameters are saved in a text format, so we have to cast them to the
347
appropriate type on loading. This function does exactly that.
349
#FIXME: we could use pickle (dumps and loads), at least in some cases
351
if param_type in cls._type_converter:
352
return cls._type_converter[param_type](param_value)
353
elif param_type == cls.TYPE_BOOL:
354
if param_value == "True":
356
elif param_value == "False":
359
raise Exception("Unrecognized bool value '%s'" %
361
elif param_type == cls.TYPE_PASSWORD:
362
if param_value == -1:
364
return Keyring().get_password(int(param_value))
365
elif param_type == cls.TYPE_LIST_OF_STRINGS:
366
the_list = param_value.split(",")
367
if not isinstance(the_list, list):
368
the_list = [the_list]
371
raise NotImplemented("I don't know what type is '%s'" %
374
def cast_param_type_to_string(self, param_type, param_value):
376
Inverse of cast_param_type_from_string
378
if param_type == GenericBackend.TYPE_PASSWORD:
379
if param_value == None:
382
return str(Keyring().set_password(
383
"GTG stored password -" + self.get_id(), param_value))
384
elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
385
if param_value == []:
387
return reduce(lambda a, b: a + "," + b, param_value)
389
return str(param_value)
393
returns the backends id, used in the datastore for indexing backends
395
return self.get_name() + "@" + self._parameters["pid"]
398
def get_human_default_name(cls):
400
returns the user friendly default backend name.
402
return cls._general_description[cls.BACKEND_HUMAN_NAME]
404
def get_human_name(self):
406
returns the user customized backend name. If the user hasn't
407
customized it, returns the default one
409
if self.KEY_HUMAN_NAME in self._parameters and \
410
self._parameters[self.KEY_HUMAN_NAME] != "":
411
return self._parameters[self.KEY_HUMAN_NAME]
413
return self.get_human_default_name()
415
def set_human_name(self, name):
417
sets a custom name for the backend
419
self._parameters[self.KEY_HUMAN_NAME] = name
420
#we signal the change
421
self._signal_manager.backend_renamed(self.get_id())
423
def is_enabled(self):
425
Returns if the backend is enabled
427
return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
430
def is_default(self):
432
Returns if the backend is enabled
434
return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
436
def is_initialized(self):
438
Returns if the backend is up and running
440
return self._is_initialized
442
def get_parameter_type(self, param_name):
444
return self.get_static_parameters()[param_name][self.PARAM_TYPE]
448
def register_datastore(self, datastore):
449
self.datastore = datastore
451
###############################################################################
452
### HELPER FUNCTIONS ##########################################################
453
###############################################################################
455
def _store_pickled_file(self, path, data):
457
A helper function to save some object in a file.
458
@param path: a relative path. A good choice is
459
"backend_name/object_name"
460
@param data: the object
462
path = os.path.join(CoreConfig().get_data_dir(), path)
465
os.makedirs(os.path.dirname(path))
466
except OSError, exception:
467
if exception.errno != errno.EEXIST:
471
with open(path, 'wb') as file:
472
pickle.dump(data, file)
473
#except pickle.PickleError:
476
def _load_pickled_file(self, path, default_value = None):
478
A helper function to load some object from a file.
479
@param path: the relative path of the file
480
@param default_value: the value to return if the file is missing or
482
@returns object: the needed object, or default_value
484
path = os.path.join(CoreConfig().get_data_dir(), path)
485
if not os.path.exists(path):
489
with open(path, 'r') as file:
490
return pickle.load(file)
491
except pickle.PickleError:
495
###############################################################################
496
### THREADING #################################################################
497
###############################################################################
499
def __try_launch_setting_thread(self):
501
Helper function to launch the setting thread, if it's not running.
503
if self.to_set_timer == None and self.is_enabled():
504
self.to_set_timer = threading.Timer(self.timer_timestep, \
505
self.launch_setting_thread)
506
self.to_set_timer.start()
508
def launch_setting_thread(self):
510
This function is launched as a separate thread. Its job is to perform
511
the changes that have been issued from GTG core. In particular, for
512
each task in the self.to_set queue, a task has to be modified or to be
513
created (if the tid is new), and for each task in the self.to_remove
514
queue, a task has to be deleted
516
while not self.please_quit:
518
task = self.to_set.pop()
523
if tid not in self.to_remove:
526
while not self.please_quit:
528
tid = self.to_remove.pop()
531
self.remove_task(tid)
532
#we release the weak lock
533
self.to_set_timer = None
535
def queue_set_task(self, task):
536
''' Save the task in the backend. '''
538
print "SETTING", task.get_title()
539
if task not in self.to_set and tid not in self.to_remove:
540
self.to_set.appendleft(task)
541
self.__try_launch_setting_thread()
543
def queue_remove_task(self, tid):
545
Queues task to be removed.
546
@param tid: The Task ID of the task to be removed
548
if tid not in self.to_remove:
549
self.to_remove.appendleft(tid)
550
self.__try_launch_setting_thread()
555
Helper method. Forces the backend to perform all the pending changes.
556
It is usually called upon quitting the backend.
558
#FIXME: this function should become part of the r/w r/o generic class
560
if self.to_set_timer != None:
561
self.please_quit = True
563
self.to_set_timer.cancel()
564
self.to_set_timer.join()
567
self.please_quit = False
568
self.launch_setting_thread()