Package GTG :: Package plugins :: Package reminder :: Module reminder
[hide private]
[frames] | no frames]

Source Code for Module GTG.plugins.reminder.reminder

  1  # -*- coding: utf-8 -*- 
  2  # Copyright (c) 2011 - Alexey Aksenov <ezh@ezh.msk.ru> 
  3  # 
  4  # This program is free software: you can redistribute it and/or modify it under 
  5  # the terms of the GNU General Public License as published by the Free Software 
  6  # Foundation, either version 3 of the License, or (at your option) any later 
  7  # version. 
  8  # 
  9  # This program is distributed in the hope that it will be useful, but WITHOUT 
 10  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
 11  # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 
 12  # details. 
 13  # 
 14  # You should have received a copy of the GNU General Public License along with 
 15  # this program.  If not, see <http://www.gnu.org/licenses/> 
 16   
 17  import gtk 
 18  import os 
 19  import re 
 20  import subprocess 
 21  import pynotify 
 22  import base64 
 23  import tempfile 
 24   
 25  from GTG import _ 
 26   
 27  #import rpdb2; 
 28  #rpdb2.start_embedded_debugger('test', fAllowRemote = True) 
 29   
30 -class Reminder:
31 PLUGIN_NAME = 'reminder' 32 VERSION = open(os.path.join(os.path.dirname(__file__), 'version')).readline() 33 NOTIFY = os.path.join(os.path.dirname(__file__), 'notify.py') 34 DEFAULT_PREFERENCES = { 35 'ask_on_task_close': False, 36 'command_open': '/usr/bin/mplayer', 37 'command_at': '/usr/bin/at', 38 'command_crontab': '/usr/bin/crontab' 39 } 40 41 kn = gtk.gdk.keyval_from_name 42 key_Return = kn("Return") 43 key_Escape = kn("Escape") 44
45 - def __init__(self):
46 pynotify.init('Getting Things GNOME! ' + self.PLUGIN_NAME) 47 self.ask_on_task_close = self.DEFAULT_PREFERENCES['ask_on_task_close'] 48 self.command_open = self.DEFAULT_PREFERENCES['command_open'] 49 self.command_at = self.DEFAULT_PREFERENCES['command_at'] 50 self.command_crontab = self.DEFAULT_PREFERENCES['command_crontab'] 51 self.reminders = {}
52
53 - def activate(self, plugin_api):
54 self.plugin_api = plugin_api 55 self.logger = self.plugin_api.get_logger() 56 #Load the preferences 57 self.preference_init() 58 self.logger.debug('the plugin v' + self.VERSION + ' is initialized')
59
60 - def onTaskOpened(self, plugin_api):
61 self.textview = plugin_api.get_textview() 62 self.logger.debug('a task was opened')
63 64 # Convert note 'special tags' to remind events
65 - def onTaskClosed(self, plugin_api):
66 textview = plugin_api.get_textview() 67 # we get the text 68 textview_start = textview.buff.get_start_iter() 69 textview_end = textview.buff.get_end_iter() 70 texte = textview.buff.serialize(textview.buff, 'application/x-gtg-task', textview_start, textview_end) 71 texte = re.sub(r'<[^>]+>', '', texte) 72 tags = map((lambda x: x.get_name().lstrip('@')), plugin_api.get_tags()) 73 alarms = list(set(tags) & set(self.get_tag_names())) 74 if (len(alarms) == 0): 75 return 76 alarms_parsed = [] 77 success_at = [] 78 unsuccess_at = [] 79 success_cron = [] 80 unsuccess_cron = [] 81 for alarm in alarms: 82 arr = texte.split('\n@' + alarm + ' ') 83 arr.pop(0) # remove title 84 for i in range(len(arr)): 85 m = re.search('\s*(#*[a-zA-Z+-:,\* /,-?#]+).*', arr[i]) 86 if (m != None): 87 arr[i] = m.group(1) 88 for time in arr: 89 if (plugin_api.get_task().get_uuid() + time.strip() in self.reminders): 90 self.logger.debug('reminder "' + time.strip() + '" already exists for task ' + plugin_api.get_task().get_uuid()) 91 continue 92 if (time[0] == '#'): 93 # cron job 94 (flag, message) = self.add_cron_job(plugin_api.get_task(), alarm, time[1:].strip()) 95 if (flag): 96 success_cron.append([alarm, message]) 97 self.reminders[plugin_api.get_task().get_uuid() + time.strip()] = True 98 else: 99 unsuccess_cron.append([alarm, message]) 100 else: 101 # at job 102 (flag, message) = self.add_at_job(plugin_api.get_task(), alarm, time.strip()) 103 if (flag): 104 success_at.append([alarm, message]) 105 self.reminders[plugin_api.get_task().get_uuid() + time.strip()] = True 106 else: 107 unsuccess_at.append([alarm, message]) 108 # generate notice message 109 notice_message = '' 110 error = False 111 if (len(success_at) > 0 or len(success_cron) > 0): 112 notice_message = _('Notification successfully updated.') 113 if (len(unsuccess_at) > 0 or len(unsuccess_cron) > 0): 114 error = True 115 notice_message = _('Some notification successfully updated.') 116 if (len(success_at) == 0 and len(success_cron) == 0 and (len(unsuccess_at) > 0 or len(unsuccess_cron) > 0)): 117 error = True 118 notice_message = _('Notification failed.') 119 if (len(success_at) > 0 or len(unsuccess_at) > 0): 120 notice_message = notice_message + _('\n ─── AT (atq for view) ───') 121 for job in success_at: 122 notice_message = notice_message + '\n@' + job[0] + ' at ' + job[1] 123 for job in unsuccess_at: 124 notice_message = notice_message + '\n@' + job[0] + ' - ' + job[1] 125 if (len(success_cron) > 0 or len(unsuccess_cron) > 0): 126 notice_message = notice_message + _('\n ─── CRON (crontab -l for view) ───') 127 for job in success_cron: 128 notice_message = notice_message + '\n@' + job[0] + ' at ' + job[1] 129 for job in unsuccess_cron: 130 notice_message = notice_message + '\n@' + job[0] + ' - ' + job[1] 131 # fire notification 132 if (notice_message != ''): 133 icon = '/usr/share/icons/gnome/48x48/status/stock_appointment-reminder.png' 134 notice = pynotify.Notification(plugin_api.get_task().get_title(), notice_message, icon) 135 if (error): 136 notice.set_timeout(0) 137 notice.show() 138 self.plugin_api.save_configuration_object(self.PLUGIN_NAME, 'reminders', self.reminders) 139 self.logger.debug('a task was closed')
140
141 - def deactivate(self, plugin_api):
142 self.logger.debug('the plugin was deactivated')
143
144 - def add_cron_job(self, task, alarm, time):
145 self.logger.debug('add cron job for task ' + task.get_uuid() + ' for alarm @' + alarm + ' at <' + time + '>') 146 # load exists crontab 147 p = subprocess.Popen(self.command_crontab + ' -l', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 148 output = '' 149 output_err = '' 150 while True: 151 stdout, stderr = p.communicate() 152 output += stdout 153 output_err += stderr 154 rc = p.poll() 155 if rc is not None: 156 break 157 if (rc == 0): 158 crontab = output 159 else: 160 self.logger.debug(output_err) 161 crontab = '' 162 # add command to crontab 163 command = '' 164 title = ' ' 165 message = ' ' 166 icon = ' ' 167 timeout = -1 168 urgency = -1 169 if self.alarmtags[alarm][1] == 0: 170 # message 171 if (self.alarmtags[alarm][2].strip() != ''): 172 title = task.get_title() 173 message = self.alarmtags[alarm][2] 174 else: 175 message = task.get_title() 176 elif self.alarmtags[alarm][1] == 1: 177 # resource 178 message = task.get_title() 179 command = self.command_open + ' ' + self.alarmtags[alarm][2] + ' &\n' 180 elif self.alarmtags[alarm][1] == 2: 181 # command 182 message = task.get_title() 183 command = self.alarmtags[alarm][2] + '\n' 184 if (title == ''): 185 title = ' ' 186 if (message == ''): 187 message = ' ' 188 if (icon == ''): 189 icon = ' ' 190 arg = 'python ' + self.NOTIFY + ' 1 ' + \ 191 task.get_uuid() + ' ' + \ 192 base64.b64encode(title) + ' ' + \ 193 base64.b64encode(message) + ' ' + \ 194 base64.b64encode(icon) + ' ' + \ 195 str(timeout) + ' ' + str(urgency) + ' ' + base64.b64encode(command) 196 crontab = crontab + '\n# gtg tag @' + alarm + ' task ' + task.get_uuid() + ' ' + task.get_title() 197 crontab = crontab + '\n' + time + ' ' + arg + '\n' 198 f = tempfile.NamedTemporaryFile(delete=False) 199 f.write(crontab) 200 f.close() 201 # add to cron 202 p = subprocess.Popen(self.command_crontab + ' ' + f.name, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 203 output = '' 204 while True: 205 stdout, stderr = p.communicate() 206 output += stdout 207 output += stderr 208 rc = p.poll() 209 if rc is not None: 210 break 211 if (rc != 0): 212 self.logger.debug(output) 213 else: 214 output = time 215 return ((rc == 0), output)
216
217 - def add_at_job(self, task, alarm, time):
218 self.logger.debug('add at job for task ' + task.get_uuid() + ' for alarm @' + alarm + ' at <' + time + '>') 219 p = subprocess.Popen(self.command_at + ' -v ' + time, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 220 command = '' 221 title = ' ' 222 message = ' ' 223 icon = ' ' 224 timeout = 0 225 urgency = -1 226 if self.alarmtags[alarm][1] == 0: 227 # message 228 if (self.alarmtags[alarm][2].strip() != ''): 229 title = task.get_title() 230 message = self.alarmtags[alarm][2] 231 else: 232 message = task.get_title() 233 elif self.alarmtags[alarm][1] == 1: 234 # resource 235 message = task.get_title() 236 command = self.command_open + ' ' + self.alarmtags[alarm][2] + ' &\n' 237 elif self.alarmtags[alarm][1] == 2: 238 # command 239 message = task.get_title() 240 command = self.alarmtags[alarm][2] + '\n' 241 if (title == ''): 242 title = ' ' 243 if (message == ''): 244 message = ' ' 245 if (icon == ''): 246 icon = ' ' 247 arg = 'python ' + self.NOTIFY + ' 0 ' + \ 248 task.get_uuid() + ' ' + \ 249 base64.b64encode(title) + ' ' + \ 250 base64.b64encode(message) + ' ' + \ 251 base64.b64encode(icon) + ' ' + \ 252 str(timeout) + ' ' + str(urgency) + ' ' + base64.b64encode(command) 253 output = '' 254 while True: 255 stdout, stderr = p.communicate(arg) 256 output += stdout 257 output += stderr 258 rc = p.poll() 259 if rc is not None: 260 break 261 self.logger.debug('execute at for tag @' + alarm + ' with rc:' + str(rc) + ' and result: ' + output) 262 return ((rc == 0), output[:output.index('\n')])
263
264 - def is_configurable(self):
265 '''A configurable plugin should have this method and return True''' 266 return True
267
268 - def configure_dialog(self, plugin_apis, manager_dialog):
269 '''Callback for configuring a plugin''' 270 self.on_grid_stop_editing() 271 self.preferences_dialog.set_transient_for(manager_dialog) 272 self.preferences_dialog.show_all()
273 274 ############################################# 275 # Preferences methods 276
277 - def preference_init(self):
278 self.preferences_load() 279 self.builder = gtk.Builder() 280 self.builder.add_from_file(os.path.dirname(os.path.abspath(__file__)) + '/reminder.ui') 281 self.preferences_dialog = self.builder.get_object('preferences_dialog') 282 self.treeview = self.builder.get_object('treeview') 283 self.liststore = self.builder.get_object('liststore') 284 self.liststoretype = self.builder.get_object('liststoretype') 285 self.button_apply = self.builder.get_object('button2') 286 self.button_add = self.builder.get_object('add') 287 self.button_delete = self.builder.get_object('delete') 288 self.button_find = self.builder.get_object('find') 289 self.button_cancel = self.builder.get_object('button1') 290 self.button_link = self.builder.get_object('linkbutton1') 291 self.button_command_open = self.builder.get_object('filechooserbutton1') 292 self.accelgroup = self.builder.get_object('accelgroup1') 293 self.builder.get_object('typecol').set_cell_data_func(self.builder.get_object('typeimage'), self.set_grid_status_icon) 294 SIGNAL_CONNECTIONS_DIC = { 295 'on_preferences_dialog_delete_event': 296 self.on_toolbar_cancel, 297 'on_btn_preferences_cancel_clicked': 298 self.on_toolbar_cancel, 299 'on_btn_preferences_ok_clicked': 300 self.on_toolbar_ok, 301 'on_btn_preferences_add_clicked': 302 self.on_toolbar_add, 303 'on_btn_preferences_del_clicked': 304 self.on_toolbar_del, 305 'on_btn_preferences_find_clicked': 306 self.on_toolbar_find, 307 'on_tag_name_changed': 308 self.on_grid_name_changed, 309 'on_tag_name_changing': 310 self.on_grid_name_changing, 311 'on_tag_type_changed': 312 self.on_grid_type_changed, 313 'on_tag_type_changing': 314 self.on_grid_type_changing, 315 'on_tag_arg_changed': 316 self.on_grid_arg_changed, 317 'on_tag_arg_changing': 318 self.on_grid_arg_changing, 319 'on_grid_stop_editing': 320 self.on_grid_stop_editing 321 } 322 self.builder.connect_signals(SIGNAL_CONNECTIONS_DIC) 323 (key, mod) = gtk.accelerator_parse("Escape") 324 self.accelgroup.connect_group(key, mod, gtk.ACCEL_VISIBLE, self.on_accel_cancel) 325 (key, mod) = gtk.accelerator_parse("Return") 326 self.accelgroup.connect_group(key, mod, gtk.ACCEL_VISIBLE, self.on_accel_apply) 327 if self.preferences.has_key('alarmtags'): 328 self.liststore.clear() 329 for row in self.preferences['alarmtags']: 330 self.liststore.append(row) 331 self.preferences_apply() 332 self.button_command_open.set_filename(self.command_open)
333
334 - def preferences_apply(self):
335 self.ask_on_task_close = self.preferences['ask_on_task_close'] 336 self.command_open = self.preferences['command_open'] 337 self.command_at = self.preferences['command_at'] 338 self.command_crontab = self.preferences['command_crontab'] 339 self.alarmtags = {} 340 self.preferences['alarmtags'] = [] 341 for row in self.liststore: 342 (tag_name, tag_type, tag_arg) = row 343 self.alarmtags[row[0]] = [tag_name, tag_type, tag_arg] 344 self.preferences['alarmtags'].append([tag_name, tag_type, tag_arg])
345
346 - def preferences_load(self):
347 data = self.plugin_api.load_configuration_object(self.PLUGIN_NAME, 'preferences') 348 if data == None or type(data) != type (dict()): 349 self.preferences = self.DEFAULT_PREFERENCES 350 else: 351 self.preferences = data 352 for key in self.DEFAULT_PREFERENCES: 353 if not key in self.preferences: 354 self.preferences[key] = self.DEFAULT_PREFERENCES[key] 355 if self.preferences.has_key('alarmtags'): 356 self.alarmtags = {} 357 for row in self.preferences['alarmtags']: 358 (tag_name, tag_type, tag_arg) = row 359 self.alarmtags[row[0]] = [tag_name, tag_type, tag_arg] 360 self.reminders = self.plugin_api.load_configuration_object(self.PLUGIN_NAME, 'reminders') 361 if self.reminders == None or type(self.reminders) != type (dict()): 362 self.reminders = {}
363
364 - def preferences_store(self):
365 self.plugin_api.save_configuration_object(self.PLUGIN_NAME, 'preferences', self.preferences)
366 367 # grid routine 368
369 - def set_grid_status_icon(self, column, cell, model, iter, *data):
370 value = model.get_value(iter, 1) 371 if value == 0: 372 cell.set_property ('stock-id', gtk.STOCK_INFO) 373 elif value == 1: 374 cell.set_property ('stock-id', gtk.STOCK_FILE) 375 elif value == 2: 376 cell.set_property ('stock-id', gtk.STOCK_EXECUTE)
377
378 - def on_grid_start_editing(self, cellrenderer = None):
379 self.editing = True 380 self.button_apply.set_sensitive(False) 381 self.button_add.set_sensitive(False) 382 self.button_delete.set_sensitive(False) 383 self.button_find.set_sensitive(False) 384 self.button_cancel.set_sensitive(False) 385 self.button_link.set_sensitive(False)
386
387 - def on_grid_stop_editing(self, cellrenderer = None, data1 = None, data2 = None):
388 self.editing = False 389 self.button_apply.set_sensitive(True) 390 self.button_add.set_sensitive(True) 391 self.button_delete.set_sensitive(True) 392 self.button_find.set_sensitive(True) 393 self.button_cancel.set_sensitive(True) 394 self.button_link.set_sensitive(True)
395
396 - def on_grid_name_changing(self, renderer, path, data = None):
397 self.on_grid_start_editing()
398
399 - def on_grid_name_changed(self, renderer, path, new_name):
400 self.on_grid_stop_editing() 401 treeiter = self.liststore.get_iter(path) 402 old_name = self.liststore.get_value(treeiter, 0) 403 if new_name == old_name: 404 return 405 collections = [r[0] for r in self.liststore] 406 if not new_name in collections: 407 if new_name.strip != '': 408 self.liststore.set(treeiter, 0, new_name) 409 else: 410 md = gtk.MessageDialog(self.preferences_dialog, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 'An entity with the same tag alreaty exists') 411 md.run() 412 md.destroy()
413
414 - def on_grid_type_changing(self, renderer, path, data = None):
415 pass
416
417 - def on_grid_type_changed(self, renderer, path, new_iter):
418 self.on_grid_stop_editing() 419 value = self.liststoretype.get_value(new_iter, 0) 420 treeiter = self.liststore.get_iter(path) 421 if value == 'message': 422 self.liststore.set(treeiter, 1, 0) 423 elif value == 'resource': 424 self.liststore.set(treeiter, 1, 1) 425 elif value == 'command': 426 self.liststore.set(treeiter, 1, 2)
427
428 - def on_grid_arg_changing(self, renderer, path, data = None):
429 self.on_grid_start_editing()
430
431 - def on_grid_arg_changed(self, renderer, path, new_arg):
432 self.on_grid_stop_editing() 433 treeiter = self.liststore.get_iter(path) 434 old_arg = self.liststore.get_value(treeiter, 2) 435 if new_arg == old_arg: 436 return 437 self.liststore.set(treeiter, 2, new_arg)
438 439 # toolbar routine 440
441 - def on_toolbar_cancel(self, widget = None, data = None):
442 for i in range(len(self.preferences['alarmtags'])): 443 self.liststore[i] = self.preferences['alarmtags'][i] 444 self.preferences_dialog.hide() 445 return True
446
447 - def on_toolbar_ok(self, widget = None, data = None):
448 #self.preferences['ask_on_task_close'] = self.chbox_minimized.get_active() 449 self.preferences['command_open'] = self.button_command_open.get_filename() 450 self.preferences_apply() 451 self.preferences_store() 452 self.preferences_dialog.hide()
453
454 - def on_toolbar_add(self, widget = None, data = None):
455 for row in self.liststore: 456 if row[0] == '': 457 md = gtk.MessageDialog(self.preferences_dialog, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 'Plain empty tag alreaty exists') 458 md.run() 459 md.destroy() 460 return 461 if len(self.liststore) == 0: 462 self.treeview.set_property('can-focus', True) 463 self.liststore.append(['', 0 , ''])
464
465 - def on_toolbar_del(self, widget = None, data = None):
466 (dummy, selection) = self.treeview.get_selection().get_selected() 467 if not selection: 468 md = gtk.MessageDialog(self.preferences_dialog, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 'Please, select tag') 469 md.run() 470 md.destroy() 471 return 472 ''' 473 Confirm dialog if the user actually wishes to delete an activity. 474 ''' 475 md = gtk.MessageDialog(self.preferences_dialog, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, 'Are you sure?') 476 result = md.run() 477 md.destroy() 478 if result == gtk.RESPONSE_YES: 479 '''Remove the row from the TreeView.''' 480 self.liststore.remove(selection) 481 if len(self.liststore) == 0: 482 self.treeview.set_property('can-focus', False)
483
484 - def on_toolbar_find(self, widget = None, data = None):
485 (dummy, selection) = self.treeview.get_selection().get_selected() 486 if not selection: 487 md = gtk.MessageDialog(self.preferences_dialog, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 'Please, select tag') 488 md.run() 489 md.destroy() 490 return 491 ''' Find file ''' 492 chooser = gtk.FileChooserDialog('Find file', self.preferences_dialog, gtk.FILE_CHOOSER_ACTION_OPEN, 493 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) 494 chooser.set_default_response(gtk.RESPONSE_OK) 495 res = chooser.run() 496 if res == gtk.RESPONSE_OK: 497 filename = chooser.get_filename() 498 try: 499 f = open(filename, 'r') 500 f.close() 501 self.logger.debug('file ' + filename + ' open') 502 except: 503 md = gtk.MessageDialog(self.preferences_dialog, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 504 ('Unable to open %s\n' % (filename))) 505 md.run() 506 md.destroy() 507 old_arg = self.liststore.get_value(selection, 2) 508 self.liststore.set(selection, 2, old_arg + filename) 509 chooser.destroy()
510 511 # common dialog
512 - def on_accel_cancel(self, accelgroup, acceleratable, accel_key, accel_mods):
513 if (self.editing == False): 514 self.on_toolbar_cancel() 515 else: 516 self.on_grid_stop_editing()
517
518 - def on_accel_apply(self, accelgroup, acceleratable, accel_key, accel_mods):
519 if (self.editing == False): 520 self.on_toolbar_ok() 521 else: 522 self.on_grid_stop_editing()
523
524 - def get_tag_names(self):
525 '''Return a list of the first-column treeview row values.''' 526 return [r[0] for r in self.preferences['alarmtags']]
527 528 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4: 529