--- /dev/null
+What's new in
+1.0.0:
+- Tickling, Deferring and Scheduling for Review now have a key of their own
+- Tickling and Deferring enable you to choose among the existing folders in the corresponding directories
+- Changed from simple list view to a view, where for each project either the first active action or its subdirectory in the @Tickled and @Someday directory is displayed
+0.9.5:
+- Automatically creates and updates configuration files
+- Active actions are set to inactive for stalling projects
+- Fixed bug where the project details view was not updated when an info was changed
+- Fixed bug where projects already set to done/tickled/someday would be scheduled for review when processing all projects
+
--- /dev/null
+del /F /S /Q C:\temp\MobileGTD
+xcopy *.py C:\temp\MobileGTD\ /R /S /Y
+rem /EXCLUDE:ensymble
+del /F /S /Q C:\temp\MobileGTD\build
+del /F /S /Q C:\temp\MobileGTD\specs
+del /F /S /Q C:\temp\MobileGTD\experimental
+@rem This is for the standard build C:\Python25\python build\ensymble.py py2sis C:\temp\MobileGTD\ --vendor="Martin Mauch" --caps=ReadUserData build\MobileGTD.sis
+@rem This is for new builds which should be able to install parallel (different UID)
+@cd "C:\Program Files\PythonForS60\"
+@C:\Python25\python "C:\Program Files\PythonForS60\ensymble.py" version
+C:\Python25\python "C:\Program Files\PythonForS60\ensymble.py" py2sis C:\temp\MobileGTD\ --uid=0xA0008CDD --vendor="Martin Mauch" --caption="MobileGTD unstable" --caps=ReadUserData+WriteUserData C:\temp\MobileGTD.unstable.sis
+
+
+
+pause
--- /dev/null
+__all__ = ["config","defaultconfig"]
--- /dev/null
+import os,re
+from defaultconfig import *
+from inout import io
+from log.logging import logger
+configuration_regexp = re.compile('(?P<key>[^:]*):(?P<value>.*)',re.U)
+
+class odict(dict):
+ def __init__(self):
+ self._keys = []
+ dict.__init__(self)
+
+ def __setitem__(self, key, item):
+ dict.__setitem__(self, key, item)
+ if key not in self._keys: self._keys.append(key)
+
+ def items(self):
+ return zip(self._keys, self.values())
+
+ def keys(self):
+ return self._keys
+ def __repr__(self):
+ return repr(self.items())
+
+ def values(self):
+ return map(self.get, self._keys)
+
+
+
+
+
+
+
+
+class Configuration(odict):
+ def __init__(self,complete_file_path,defaults={}):
+ odict.__init__(self)
+ self.file_path=complete_file_path
+
+ self.read()
+ if self.merge(defaults):
+ self.write()
+ self.read()
+ def read(self):
+ encoded_path = io.os_encode(self.file_path)
+ if not os.path.isfile(encoded_path):
+ logger.log(u'Configuration file %s does not exist'%os.path.abspath(encoded_path))
+ return
+ for line in io.parse_file_to_line_list(self.file_path):
+ if len(line)<1:continue
+ if line[0] == '#': continue
+ matching = configuration_regexp.match(line)
+ key = matching.group('key')
+ value = matching.group('value').rstrip(u' \r\n')
+
+ self[key]=self.parse_value(value)
+ def parse_value(self,value):
+ if ',' in value:
+ value=value.split(',')
+ return value
+
+ def merge(self, other):
+ changed = False
+ for key in other:
+ if key not in self:
+ self[key] = other[key]
+ changed = True
+ return changed
+
+
+ def write(self):
+ content = u'\n'.join([u'%s:%s'%(key,self.format_value(value)) for (key,value) in self.items()])
+ io.write(self.file_path,content)
+ def format_value(self,value):
+ if isinstance(value,list):
+ return ','.join(value)
+ else:
+ return value
+
+
+
+COMMON_CONFIG = Configuration(main_config_file,default_configuration)
+ABBREVIATIONS = {} #Configuration("abbreviations.cfg",default_abbreviations)
+
+def read_configurations():
+ global ABBREVIATIONS
+ ABBREVIATIONS = Configuration("abbreviations.cfg",default_abbreviations)
+
+gtd_directory = COMMON_CONFIG['path']
+inactivity_threshold = int(COMMON_CONFIG['inactivity_threshold'])
+read_sms = int(COMMON_CONFIG['read_sms'])
+
+__all__=["Configuration","read_sms","inactivity_threshold","COMMON_CONFIG"]
--- /dev/null
+main_config_file = 'C:/System/Data/mobile_gtd.cfg'
+
+default_configuration = {"screen":"normal",
+"path":"C:/Data/GTD/",
+"inactivity_threshold":"7",
+"read_sms":"1",
+"action_editor":"form"
+}
+
+default_actions_menu = {
+"switch_entry_filter":"1,Toggle Active/All/Inactive Actions",
+"add_action":"2,Add Action",
+"add_info":"4,Add Info",
+"change_status":"7,Change Status",
+#"change":"8,Change Text",
+"search_item":"0,Search Item",
+"edit_menu":"5,Edit menu configuration",
+"remove":"Backspace,Remove Item",
+}
+
+default_projects_menu = {
+"switch_entry_filter":"1,Toggle Active/All/Inactive Projects",
+"activate":"2,Schedule as active",
+"defer":"3,Defer Project",
+"reread_projects":"4,Reread Projects",
+"process_all":"6,Process all Projects",
+"review":"7,Review Project",
+"tickle":"8,Tickle project",
+"rename":"9,Rename project",
+"search_item":"0,Search Item",
+"remove":"Backspace,Set project to done",
+"new_project":"Star,Create new project",
+"new_action":"Hash,Create new action",
+"edit_menu":"5,Edit menu configuration",
+"edit_config":"5,Edit global configuration"
+}
+
+
+default_abbreviations = {
+"1":"Agenda/",
+"2":"Computer/",
+"26":"Computer/Online/",
+"26":"Computer/Online/Mail ",
+"3":"Errands/",
+"4":"Anywhere/",
+"42":"Anywhere/Brainstorm/",
+"47":"Anywhere/Phone/",
+"46":"Anywhere/MobilePhone/",
+"9":"WaitingFor/"
+}
--- /dev/null
+# SYMBIAN_UID = 0xA0008CDD
+sys.path.append('c:\\private\\a0008cdd')
+import main
--- /dev/null
+__all__ = ["gui"]
--- /dev/null
+from config.config import *
+from model.projects import Projects
+
+import appuifw
+import thread
+from log.logging import logger
+from e32 import Ao_lock, in_emulator
+from key_codes import *
+import key_codes
+
+
+def show_config(cfg):
+ fields = []
+ for k, v in cfg.items():
+ v = cfg.format_value(v)
+ if isinstance(v, int) or isinstance(v, long):
+ tname = 'number'
+ v = int(v)
+ elif isinstance(v, list) or isinstance(v, tuple):
+ for item in v[0]:
+ if not isinstance(item, unicode):
+ raise Exception("list can contain only unicode objects, "\
+ "object %r is not supported" % item)
+
+ tname = 'combo'
+ elif isinstance(v, unicode):
+ tname = 'text'
+ else:
+ raise Exception("%s has non-supported value" % k)
+
+ fields.append((unicode(k), tname, v))
+
+
+ form = appuifw.Form(fields=fields, flags=appuifw.FFormEditModeOnly | \
+ appuifw.FFormDoubleSpaced)
+
+ saved = [False]
+ def save_hook(param):
+ saved[0] = True
+ form.save_hook = save_hook
+
+ form.execute()
+
+ # return true if user saved, false otherwise
+ if not saved[0]:
+ return False
+
+ for label, tname, value in form:
+ if tname == 'combo':
+ value = (value[0], int(value[1]))
+
+ cfg[str(label)] = cfg.parse_value(value)
+
+ return True
+
+
+def no_action():
+ pass
+
+def applicable_functions(obj,allowed_function_names):
+ function_names = [function_name for function_name in dir(obj) if function_name in allowed_function_names]
+ return [eval('obj.%s'%function_name) for function_name in function_names]
+
+def get_key(key_name):
+ if key_name == '':
+ key = None
+ else:
+ key=eval('EKey%s'%key_name)
+ return key
+
+def all_key_names():
+ return filter(lambda entry:entry[0:4]=='EKey',dir(key_codes))
+def all_key_values():
+ key_values=[
+ EKey0,
+ EKey1,
+ EKey2,
+ EKey3,
+ EKey4,
+ EKey5,
+ EKey6,
+ EKey7,
+ EKey8,
+ EKey9,
+ EKeyStar,
+ EKeyHash,
+ ]
+ return key_values
+
+
+def save_gui(object):
+ object.old_gui = appuifw.app.body
+ object.old_menu = appuifw.app.menu
+ object.old_exit_key_handler = appuifw.app.exit_key_handler
+ object.old_title=appuifw.app.title
+ object.lock = Ao_lock()
+
+def restore_gui(object):
+ appuifw.app.body = object.old_gui
+ appuifw.app.menu = object.old_menu
+ appuifw.app.exit_key_handler = object.old_exit_key_handler
+ appuifw.app.title = object.old_title
+
+
+class ListView(object):
+ def __init__(self,title):
+ self.title = title
+ self.view = appuifw.Listbox(self.items(),self.change_entry)
+
+ def change_entry(self):
+ pass
+
+ def run(self):
+ self.adjustment = None
+ appuifw.app.screen=COMMON_CONFIG['screen'].encode('utf-8')
+ save_gui(self)
+ appuifw.app.title=self.title
+ appuifw.app.body=self.view
+ appuifw.app.exit_key_handler=self.exit
+ try:
+ self.lock.wait()
+ while not self.exit_flag:
+ self.refresh()
+ self.lock.wait()
+ except:
+ pass
+ restore_gui(self)
+ def exit(self):
+ self.exit_flag = True
+ self.lock.signal()
+
+ def update(self,subject=None):
+ #logger.log(u'Updated %s'%repr(self))
+ if self.lock:
+ self.lock.signal()
+ #pass
+
+ def index_changed(self,adjustment=None):
+ if adjustment:
+ index = self.selected_index() + adjustment
+ else:
+ index = self.selected_index()
+ if index < 0:
+ index = len(self.widgets) - 1
+ if index >= len(self.widgets):
+ index = 0
+ self.set_bindings_for_selection(index)
+
+ def refresh(self):
+ appuifw.app.menu=self.get_menu_entries()
+
+ def set_index(self,index):
+ if index > len(self.widgets):
+ index = len(self.widgets)
+ if index < 0:
+ index = 0
+ self.view.set_list(self.items(),index)
+
+ def selected_index(self):
+ return self.view.current()
+
+
+class WidgetBasedListView(ListView):
+ def __init__(self,title):
+ self.widgets = self.generate_widgets()
+ super(WidgetBasedListView,self).__init__(title)
+ self.exit_flag = False
+
+ def run(self):
+ self.refresh()
+ self.set_bindings_for_selection(0)
+ ListView.run(self)
+
+ def notify(self,object,attribute,new=None,old=None):
+ self.refresh()
+ def refresh(self):
+ self.widgets = self.generate_widgets()
+ self.redisplay_widgets()
+ super(WidgetBasedListView,self).refresh()
+ def redisplay_widgets(self):
+ self.set_index(self.selected_index())
+ def items(self):
+ return self.all_widget_texts()
+ def all_widget_texts(self):
+ return [entry.list_repr() for entry in self.widgets]
+
+
+
+ def current_widget(self):
+ return self.widgets[self.selected_index()]
+
+
+class KeyBindingView(object):
+
+ def __init__(self,binding_map):
+ self.binding_map = binding_map
+
+ def get_menu_entries(self):
+ menu_entries=[]
+ for key,key_name,description,function in self.key_and_menu_bindings(self.selected_index()):
+ if description != '':
+ if key:
+ if key_name == 'Backspace': key_name='C'
+ description='[%s] '%key_name +description
+ else:
+ description=' '+description
+ menu_entries.append((description,function))
+ menu_entries.append((u'Exit', self.exit))
+ return menu_entries
+ def set_bindings_for_selection(self,selected_index):
+ self.remove_all_key_bindings()
+
+ for key,key_name,description,function in self.key_and_menu_bindings(selected_index):
+ if key:
+ self.view.bind(key,function)
+ self.view.bind(EKeyUpArrow,lambda: self.index_changed(-1))
+ self.view.bind(EKeyDownArrow,lambda: self.index_changed(1))
+
+ def remove_all_key_bindings(self):
+ for key in all_key_values():
+ self.view.bind(key,no_action)
+
+class SearchableListView(WidgetBasedListView):
+ def __init__(self,title,entry_filters):
+ self.current_entry_filter_index = 0
+ self.entry_filters = entry_filters
+ self.filtered_list = self.entry_filters[0]
+ self.lock = None
+ super(SearchableListView,self).__init__(title)
+
+
+ def search_item(self):
+ selected_item = appuifw.selection_list(self.all_widget_texts(),search_field=1)
+ if selected_item == None or selected_item == -1:
+ selected_item = self.selected_index()
+ self.view.set_list(self.items(),selected_item)
+ self.set_bindings_for_selection(selected_item)
+ def switch_entry_filter(self):
+ self.current_entry_filter_index += 1
+ self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
+ self.refresh()
+
+
+class EditableListView(SearchableListView,KeyBindingView):
+ def __init__(self,title,entry_filters,binding_map):
+ KeyBindingView.__init__(self,binding_map)
+ super(EditableListView, self).__init__(title,entry_filters)
+
+ def key_and_menu_bindings(self,selected_index):
+ key_and_menu_bindings=[]
+ for function in applicable_functions(self.widgets[selected_index],self.binding_map)+\
+ applicable_functions(self,self.binding_map):
+ execute_and_update_function = self.execute_and_update(function)
+ (key,description) = self.binding_map[function.__name__]
+ key_and_menu_bindings.append((get_key(key),key,description,execute_and_update_function))
+ return key_and_menu_bindings
+
+ def change_entry(self):
+ self.current_widget().change()
+ self.refresh()
+ def execute_and_update(self,function):
+ return lambda: (function(),self.refresh(),self.index_changed())
+
+ def notify(self,item,attribute,new=None,old=None):
+ self.refresh()
+
+#class DisplayableFunction:
+# def __init__(self,display_name,function):
+# self.display_name = display_name
+# self.function = function
+# def list_repr(self):
+# return self.display_name
+# def execute(self):
+# function()
+
+# Public API
+__all__= ('EditableListView','show_config')
--- /dev/null
+__all__ = ["gui","project_view"]
--- /dev/null
+from log.logging import logger
+from model.action import *
+import appuifw
+
+def edit_action(action):
+ f = ActionView(action)
+ f.execute()
+ return f.isSaved() == 1
+
+class ActionView( object ):
+
+ def __init__( self, action):
+ self.action = action
+ self.saved = False
+
+
+
+
+ ## Displays the form.
+ def execute( self ):
+ self.saved = False
+ fields = [(u'Context','text',self.action.context),
+ (u'Description','text',self.action.description),
+ (u'Info','text',self.action.info)]
+ logger.log(repr(fields))
+ self.form = appuifw.Form(fields, appuifw.FFormEditModeOnly)
+
+ self.form.save_hook = self.markSaved
+ self.form.flags = appuifw.FFormEditModeOnly
+ self.form.execute( )
+ if self.saved:
+ self.save_fields()
+
+ def save_fields(self):
+ context = self.get_context()
+ if len(context.strip()) == 0:
+ context,description,info,status = parse_action_line(self.get_description())
+ else:
+ description = self.get_description()
+ context = parse_context(context)
+
+ if len(self.get_info().strip()) > 0:
+ info = self.get_info()
+ else:
+ info = u""
+ self.action.context = context
+ self.action.description = description
+ self.action.info = info
+
+ ## save_hook send True if the form has been saved.
+ def markSaved( self, saved ):
+ #appuifw.note(u'save_hook called with %s'%saved)
+
+ if saved and self.is_valid():
+ self.saved = True
+ return self.saved
+ def isSaved( self ):
+ return self.saved
+
+ def get_description( self ):
+ return unicode(self.form[1][2])
+
+
+
+ def get_context( self ):
+ return unicode(self.form[0][2])
+ def is_valid(self):
+ return len(self.form[0]) > 2 and len(self.form[1]) > 2
+
+
+ def get_info( self ):
+ return unicode(self.form[2][2])
--- /dev/null
+from action_view import edit_action
+from model.action import *
+class ActionWidget:
+ def __init__(self,action):
+ self.action = action
+
+ def change(self):
+ if edit_action(self.action):
+ self.action.status = active
+
+ def change_status(self):
+
+ if self.action.status == active:
+ self.action.status = done
+ elif self.action.status == done:
+ self.action.status = inactive
+ else:
+ self.action.status = active
+ def list_repr(self):
+ return self.action.__str__()
--- /dev/null
+from model.action import Action
+from action_view import edit_action
+class ContextWidget:
+ def __init__(self,context,project):
+ self.context = context
+ self.project = project
+ def change(self):
+ a = Action(self.project.name,self.context,self.project)
+ if edit_action(a):
+ self.project.add_action(a)
+ def list_repr(self):
+ return u'@%s'%self.context
--- /dev/null
+import project_view
+
+
+class InfoWidget:
+ def __init__(self,info,project):
+ self.info = info
+ self.project = project
+ def remove(self):
+ self.project.remove_info(self.info)
+ def change(self):
+ new_info=project_view.ask_for_info(self.info.gui_string())
+ if new_info:
+ self.info.text = new_info
+ self.project.dirty=True
+
+ def list_repr(self):
+ return u' %s'%self.info
--- /dev/null
+from model.info import Info
+import project_view
+
+class InfosWidget:
+ def __init__(self,project):
+ self.project = project
+ def change(self):
+ info = project_view.ask_for_info(self.project.name)
+ if info:
+ self.project.add_info(Info(info),0)
+ self.project.dirty=True
+ def list_repr(self):
+ return u'Infos'
--- /dev/null
+import model, config, gui
+import appuifw,os
+import thread
+from model.model import *
+from config.config import gtd_directory,Configuration
+from config.defaultconfig import default_actions_menu
+from gui.gui import EditableListView
+from infos_widget import InfosWidget
+from info_widget import InfoWidget
+from context_widget import ContextWidget
+from action_widget import ActionWidget,edit_action
+from action_view import ActionView
+from model import action
+from model import info
+from model import project
+from model.action import Action
+from log.logging import logger
+from e32 import Ao_lock, in_emulator
+from key_codes import *
+import key_codes
+
+from gui import *
+
+ACTION_LIST_KEYS_AND_MENU = Configuration(os.path.join(gtd_directory,"actions.cfg"))
+
+
+def ask_for_action(project_name,proposition=None):
+
+ action = Action(project_name,u'')
+ was_saved = edit_action(action)
+ if was_saved:
+ return action
+ else:
+ return None
+
+
+#def ask_for_action(project_name,proposition=None):
+# if proposition == None:
+# proposition = u'Context %s'%(project_name)
+# action_line = appuifw.query(u'Enter action %s'%project_name,'text',proposition)
+# if action_line == None:
+# return None
+# else:
+# return parse_action(action_line)
+
+
+# text = u'Enter action'
+# if info:
+# text = text+info
+# new_description = appuifw.query(text,'text',action.description)
+# if new_description:
+# action.set_description(new_description)
+# return True
+# return False
+
+
+
+def ask_for_info(proposition):
+ return appuifw.query(u'Enter info','text',u'%s'%(proposition))
+
+
+class ProjectView(EditableListView):
+ def __init__(self,project):
+ self.project = project
+ self.project.observers.append(self)
+ super(ProjectView, self).__init__(self.project.name, [lambda:self.project.actions.with_property(lambda a:a.status==action.active)], ACTION_LIST_KEYS_AND_MENU)
+
+ def exit(self):
+ self.project.observers.remove(self)
+# self.project.status = project.active
+# self.project.status.update(self.project)
+ EditableListView.exit(self)
+# self.project.dirty = True
+# self.project.write()
+# self.projects.update_status(self.project)
+ def edit_menu(self):
+ show_config(ACTION_LIST_KEYS_AND_MENU)
+ ACTION_LIST_KEYS_AND_MENU.write()
+
+ def actions(self):
+ return self.project.actions
+
+ def generate_widgets(self):
+ widgets = []
+ widgets.append(InfosWidget(self.project))
+ for info in self.project.infos:
+ widgets.append(InfoWidget(info,self.project))
+ for (context,actions) in self.actions_by_context().items():
+ widgets.append(ContextWidget(context,self.project))
+ for action in actions:
+ widgets.append(ActionWidget(action))
+ return widgets
+ def actions_by_context(self):
+ context_actions_map = {}
+ for action in self.actions():
+ if not action.context in context_actions_map:
+ context_actions_map[action.context]=[]
+ context_actions_map[action.context].append(action)
+ return context_actions_map
+
+ def add_action(self):
+ a = ask_for_action(self.project.name)
+ if a:
+ self.project.add_action(a)
+ self.project.status = project.active
+ def add_info(self):
+ i = ask_for_info(self.project.name)
+ if i:
+ selected = self.selected_index()
+ # First position is "Infos"
+ if selected>=0 and selected <= len(self.project.infos):
+ position = selected
+ else:
+ position = None
+ self.project.add_info(info.Info(i),position)
+ if position:
+ self.set_index(position+1)
+
+
+
+ def notify(self,project,attribute,new=None,old=None):
+ self.refresh()
+
+
+
+
+__all__= ('ProjectView','ask_for_action','ask_for_info')
--- /dev/null
+__all__ = ["project_list_view"]
--- /dev/null
+
+class NewActionWidget:
+ def change(self):
+ from gui.project_details.project_view import ask_for_action
+ action = ask_for_action(u"No project")
+ if action:
+ action.process()
+ return action
+
+ def list_repr(self):
+ return u'New action'
+ def name_and_details(self):
+ return (self.list_repr(), u'Sure? No project attached?')
--- /dev/null
+import appuifw
+from log.logging import logger
+from project_widget import ProjectWidget
+from model.project import Project
+from model.info import Info
+class NewProjectWidget:
+ def __init__(self,projects):
+ self.projects = projects
+ def change(self,proposed_name = 'Project name',infos=None):
+ project_name = unicode(appuifw.query(u'Enter a name for the project','text',proposed_name))
+ logger.log(u'New project: %s'% project_name)
+ if not project_name:
+ return
+ project = Project(project_name)
+ self.projects.append(project)
+ if infos:
+ for info in infos:
+ project.add_info(Info(info))
+ ProjectWidget(self.projects,project).change()
+ return project
+
+ def list_repr(self):
+ return u'New project'
+ def name_and_details(self):
+ return (self.list_repr(), u'')
--- /dev/null
+from model import model
+from config import config
+from gui.gui import EditableListView
+import appuifw,thread,re
+from model import *
+from config.config import gtd_directory,read_sms
+from config.defaultconfig import default_projects_menu
+from log.logging import logger
+from e32 import Ao_lock, in_emulator
+from key_codes import *
+import key_codes
+from new_project_widget import NewProjectWidget
+from new_action_widget import NewActionWidget
+from project_widget import ProjectWidget
+from logic import review_visitor
+
+#from gui import *
+
+
+PROJECT_LIST_KEYS_AND_MENU = config.Configuration(gtd_directory+"projects.cfg",default_projects_menu)
+sms_regexp = re.compile('([^\w ]*)',re.U)
+
+class ProjectListView(EditableListView):
+ def __init__(self,projects):
+ self.projects = projects
+ self.projects.observers.append(self)
+ super(ProjectListView, self).__init__(u'Projects', [lambda:projects],PROJECT_LIST_KEYS_AND_MENU)
+ #appuifw.note(u'Before starting thread')
+# thread.start_new_thread(projects.process,())
+ #appuifw.note(u'After starting thread %s'%repr(projects.observers))
+ def exit(self):
+ self.exit_flag = True
+ self.lock.signal()
+ if not in_emulator():
+ appuifw.app.set_exit()
+
+ def edit_menu(self):
+ show_config(PROJECT_LIST_KEYS_AND_MENU)
+ PROJECT_LIST_KEYS_AND_MENU.write()
+ def edit_config(self):
+ show_config(COMMON_CONFIG)
+ COMMON_CONFIG.write()
+ def add_project(self,project):
+ self.projects.add_project(project)
+ self.refresh()
+ def all_widget_entries(self):
+ return [entry.name_and_details() for entry in self.widgets]
+ def new_project(self):
+ NewProjectWidget(self.projects).change()
+ def new_action(self):
+ NewActionWidget().change()
+ def generate_widgets(self):
+ widgets = []
+ widgets.append(NewProjectWidget(self.projects))
+ widgets.append(NewActionWidget())
+ self.filtered_list()
+ widgets.extend([ProjectWidget(self.projects,project) for project in self.projects.sorted_by_status()])
+ if read_sms:
+ from sms_widget import create_sms_widgets
+ try:
+ widgets.extend(create_sms_widgets())
+ except Exception,e:
+ logger.log(u'No permission to access SMS inbox')
+ logger.log(unicode(repr(e.args)))
+ return widgets
+
+ def process_all(self):
+ review_visitor.reviewer.review(self.projects)
+ self.redisplay_widgets()
+ def reread_projects(self):
+ self.projects.reread()
+ self.refresh()
+
+
+
+
--- /dev/null
+import appuifw #Only temporary
+from model import project,datetime
+import time
+class ProjectWidget:
+ def __init__(self,projects,project):
+ self.project = project
+ self.projects = projects
+ def change(self):
+# appuifw.note(u'Opening')
+ from gui.project_details.project_view import ProjectView
+ edit_view = ProjectView(self.project)
+ edit_view.run()
+
+ def add_action(self):
+ action = ask_for_action(u'for project %s'%self.project.name())
+ if action:
+ action.process()
+ add_action_to_project(action,self.project)
+ self.project.write()
+
+ def add_info(self):
+ info = ask_for_info(self.project.name())
+ if info:
+ self.project.add_info(Info(info))
+ self.project.write()
+ def review(self):
+ self.projects.review(self.project)
+ def activate(self):
+ self.project.status = project.active
+ def process(self):
+ appuifw.note(u'Processing %s'%self.project.name())
+ self.projects.process(self.project)
+
+ def rename(self):
+ new_name = appuifw.query(u'Enter new project name','text',u'%s'%self.project.name())
+ if new_name != None:
+ self.project.set_name(new_name)
+ def remove(self):
+ self.project.status = project.done
+ def list_repr(self):
+ return self.project.status_symbol_and_name()
+ def name_and_details(self):
+ if self.project.has_active_actions():
+ details=self.project.active_actions()[0].summary()
+ else:
+ details=u'Something' #self.project.additional_info()
+ return (self.list_repr(),details)
+
+
+ def tickle(self):
+ t = appuifw.query(u'Enter the date when the project should show up again', 'date', time.time())
+ if t:
+ date_struct = time.gmtime(t)
+ date = datetime.date(date_struct[0],date_struct[1],date_struct[2])
+ print date
+ self.project.status = project.Tickled(date)
+ def defer(self):
+ self.choose_and_execute(self.projects.get_someday_contexts(),self.projects.defer)
+ def choose_and_execute(self,choices,function):
+ if choices==None or len(choices)==0:
+ function(self.project)
+ return
+ selected_item = appuifw.selection_list(choices,search_field=1)
+
+ if not selected_item==None:
+ function(self.project,choices[selected_item])
+
+ def review(self):
+ self.project.status = project.inactive
--- /dev/null
+from inbox import EInbox,Inbox
+INBOX = Inbox(EInbox)
+
+def create_sms_widgets():
+ return [SMSWidget(sms_id,self.projects) for sms_id in INBOX.sms_messages()]
+
+class SMSWidget:
+ def __init__(self,sms_id,projects):
+ self.sms_id = sms_id
+ self.projects = projects
+ def content(self):
+ return INBOX.content(self.sms_id)
+ def change(self):
+ self.view_sms()
+ def create_project(self):
+ infos = []
+ lines = sms_regexp.split(self.content())
+
+ info_lines = []
+ for index in range(len(lines)):
+ if len(lines[index]) < 2 and index>0:
+ previous = info_lines.pop()
+ info_lines.append(previous+lines[index])
+ else:
+ info_lines.append(lines[index])
+ for line in info_lines:
+ infos.append(line)
+ project = NewProjectWidget(self.projects).change(u'Project for SMS from %s'%self.sender(),infos)
+ def remove(self):
+ INBOX.delete(self.sms_id)
+ def sender(self):
+ return INBOX.address(self.sms_id)
+ def list_repr(self):
+ return u'SMS from %s'%self.sender()
+ def name_and_details(self):
+ return (self.list_repr(), self.content())
+
+ def view_sms(self):
+ save_gui(self)
+ t = appuifw.Text()
+ t.add(self.list_repr())
+ t.add(u':\n')
+ t.add(self.content())
+ appuifw.app.menu=[(u'Create Project', self.create_project),
+ (u'Exit', self.exit_sms_view)]
+
+ appuifw.app.title=self.list_repr()
+ appuifw.app.body=t
+ appuifw.app.exit_key_handler=self.exit_sms_view
+ lock = Ao_lock()
+ def exit_sms_view(self):
+ self.lock.signal()
+ restore_gui(self)
--- /dev/null
+import os,sys
+
+
+def create_dir_if_necessary(path):
+ if len(path) > 0 and not os.path.exists(path):
+ os.makedirs(path)
+
+def safe_chdir(path):
+ #try:
+ # path = os_encode(path_unicode)
+ #except UnicodeError:
+ # #logger.log('Error decoding path %s'%repr(path_unicode))
+ # print 'Error decoding path %s'%repr(path_unicode)
+ # path = path_unicode
+ create_dir_if_necessary(path)
+ os.chdir(path)
+
+def create_file(file_path):
+ dir = os.path.dirname(os_encode(file_path))
+ create_dir_if_necessary(dir)
+ encoded_file_path = os_encode(file_path)
+ file_name = os.path.join(dir,os.path.basename(encoded_file_path))
+
+ f = file(file_name,'w')
+ return f
+
+
+def os_encode(s):
+ return s.encode(sys.getfilesystemencoding())
+
+def os_decode(s):
+ if type(s) == unicode:
+ return s
+ else:
+ return unicode(s,sys.getfilesystemencoding())
+
+def write(file_path,content):
+ f = create_file(file_path)
+ f.write(os_encode(content))
+ f.close()
+ from log.logging import logger
+ logger.log(u'Wrote %s to %s'%(content,os.path.abspath(file_path)))
+
+def list_dir(root,recursive=False,filter=None):
+ encoded_root = os_encode(root)
+ if not os.path.exists(encoded_root):
+ return []
+ all_files_and_dirs = []
+ for name in os.listdir(encoded_root):
+ file_name = os_decode(os.path.join(encoded_root,name))
+ if recursive and os.path.isdir(os_encode(file_name)):
+ all_files_and_dirs.extend(list_dir(file_name, True,filter))
+ if (not filter) or filter(file_name):
+ all_files_and_dirs.append(file_name)
+ return all_files_and_dirs
+
+def guess_encoding(data):
+ #from logging import logger
+ encodings = ['ascii','utf-8','utf-16']
+ successful_encoding = None
+ if data[:3]=='\xEF\xBB\xBF':
+ data = data[3:]
+
+ for enc in encodings:
+ if not enc:
+ continue
+ try:
+ decoded = unicode(data, enc)
+ successful_encoding = enc
+ break
+ except (UnicodeError, LookupError):
+ pass
+ if successful_encoding is None:
+ raise UnicodeError('Unable to decode input data %s. Tried the'
+ ' following encodings: %s.' %(repr(data), ', '.join([repr(enc)
+ for enc in encodings if enc])))
+ else:
+ #logger.log('Decoded %s to %s'%(repr(data),repr(decoded)),6)
+ return (decoded, successful_encoding)
+
+
+def read_text_from_file(unicode_file_name):
+ from log.logging import logger
+ file_name = os_encode(unicode_file_name)
+# logger.log(u'Reading from %s'%os.path.abspath(file_name).decode('utf-8'))
+ f=file(file_name,'r')
+ raw=f.read()
+ f.close()
+ (text,encoding)=guess_encoding(raw)
+
+ return text
+def parse_file_to_line_list(unicode_complete_path):
+ text = read_text_from_file(unicode_complete_path)
+ lines = text.splitlines()
+ return lines
+def is_dir(unicode_path):
+ return os.path.isdir(os_encode(unicode_path))
--- /dev/null
+import os
+from inout import io
+
+class FileLogger:
+ def __init__(self,file_path=u'C:/mobile_gtd.log',log_level = 8):
+ self.entries = []
+ self.file_path = file_path
+ self.file_path = 'C:/Data/GTD/mobile_gtd.log'
+ self.log_level = log_level
+ #self.log_file = file(io.os_encode(file_path),'w')
+ io.create_file(self.file_path).close
+ self.log_file = file(self.file_path,'a')
+ def log_stderr(self):
+ import sys
+ self.old_stderr = sys.stderr
+ sys.stderr = self.log_file
+ sys.stderr.write('stderr logged from logging\n')
+ self.log_file.flush()
+
+ def unlog_stderr(self):
+ import sys
+ sys.stderr = self.old_stderr
+ def log(self,text,level=0):
+ if level < self.log_level:
+ self.log_file.write(io.os_encode(text)+'\n')
+ self.log_file.flush()
+ def close(self):
+ #sys.stderr.flush()
+ self.unlog_stderr()
+ self.log(u'Closing log')
+ self.log_file.flush()
+ self.log_file.close()
+
+
+
+class ConsoleLogger:
+ def __init__(self,log_level = 8):
+ self.log_level = log_level
+ def log(self,text,level=0):
+ import appuifw
+ if level < self.log_level:
+ appuifw.note(u''+repr(text))
+ def close(self):
+ pass
+
+class NullLogger:
+ def log(self,text,level=0):
+ pass
+ def log_stderr(self):
+ pass
+ def close(self):
+ pass
+
+#logger=FileLogger(gtd_directory+'gtd.log')
+# Need NullLogger during initialization of FileLogger
+#logger=NullLogger()
+logger=FileLogger()
+#logger=ConsoleLogger()
--- /dev/null
+import sys
+import linecache
+from e32 import ao_sleep
+refresh=lambda:ao_sleep(0)
+
+class trace:
+ def __init__(self,f_all=u'c:\\traceit.txt',f_main=u'c:\\traceitmain.txt'):
+ self.out_all=open(f_all,'w')
+ self.out_main=open(f_main,'w')
+
+ def go(self):
+ sys.settrace(self.traceit)
+
+ def stop(self):
+ sys.settrace(None)
+ self.out_all.close()
+ self.out_main.close()
+
+ def traceit(self,frame, event, arg):
+ lineno = frame.f_lineno
+ name = frame.f_globals["__name__"]
+ file_trace=frame.f_globals["__file__"]
+ line=linecache.getline(file_trace,lineno)
+
+ self.out_all.write("%s*%s*of %s(%s)\n*%s*\n" %(event,lineno,name,file_trace,line.rstrip()))
+ refresh()
+ return self.traceit
+
+
+
--- /dev/null
+from model import action
+from model import project
+from model import datetime
+
+
+def update_status(e):
+ old_status = e.status
+ new_status = e.status.update(e)
+ if new_status != old_status:
+ e.status = new_status
+
+
+class ReviewVisitor(object):
+ def review(self,projects):
+ for p in projects:
+ for a in p.actions:
+ update_status(a)
+# for a in p.actions_with_status(action.active):
+# a.status = action.done
+ update_status(p)
+ if p.last_modification_date() <= datetime.date.in_x_days(-5):
+ p.status = project.inactive
+
+reviewer = ReviewVisitor()
--- /dev/null
+# SYMBIAN_UID = 0xA0008CDC
+# 0xA0008CDC
+# 0x2001A1A0
+
+#from logging import traceS60
+#tr=traceS60.trace()
+#tr.go()
+
+def run():
+ import sys
+ import e32
+ if e32.in_emulator():
+ sys.path.append('c:/python/')
+
+
+ import os.path
+ print sys.path
+# print os.path.dirname(__file__)
+# sys.path.append(os.path.dirname(__file__))
+ import log.logging
+ from log.logging import logger
+ import sys,os
+ logger.log_stderr()
+ sys.stderr.write('stderr logged from default')
+
+
+
+ lock=None
+
+ from config.config import gtd_directory,read_configurations
+ read_configurations()
+
+ from inout.io import safe_chdir
+ safe_chdir(gtd_directory)
+ print os.getcwd()
+ try:
+ e32.ao_yield()
+ import sys,os
+
+ import config.config, config.defaultconfig
+ import gui.gui
+ from model.projects import Projects
+ from gui.projects_list.project_list_view import ProjectListView
+ import inout.io
+ from persistence.projects_directory import ProjectsDirectory
+
+ directory = os.path.join(config.config.gtd_directory,'@Projects')
+
+ projects = Projects()
+ projects_directory = ProjectsDirectory(projects)
+ projects_directory.add_directory(directory)
+ projects_directory.add_directory(os.path.join(directory,'@Review'))
+ projects_directory.read()
+# projects.process()
+ projects_view = ProjectListView(projects)
+ projects_view.run()
+ #logger.close()
+ except Exception, e:
+ import appuifw,traceback
+ trace = traceback.extract_tb(sys.exc_info()[2])
+ print e,trace
+ def display(objects):
+ strings=[]
+ for object in objects:
+ strings.append(u'%s'%object)
+ appuifw.selection_list(strings)
+
+ error_text = unicode(repr(e.args))
+ t = appuifw.Text()
+ for trace_line in trace:
+ formatted_trace_line = u'\nIn %s line %s: %s "%s"'%trace_line
+ logger.log(formatted_trace_line,1)
+ t.add(formatted_trace_line)
+ logger.log(error_text,1)
+ t.add(error_text)
+ lock = e32.Ao_lock()
+ appuifw.app.menu=[(u'Exit', lock.signal)]
+
+ appuifw.app.title=u'Error'
+ appuifw.app.body=t
+ #appuifw.app.exit_key_handler=gui.exit
+ lock.wait()
+
+ logger.close()
+
+if __name__ == "__main__":
+ run()
+#tr.stop()
--- /dev/null
+import re
+from model import *
+import config.config as config
+
+class ActionStatus(Status):
+ pass
+
+class UnprocessedStatus(Status):
+ def __init__(self):
+ super(UnprocessedStatus,self).__init__(u'unprocessed',0)
+ def update(self,owner):
+ return active
+
+
+
+unprocessed = UnprocessedStatus()
+active = ActionStatus(u'active',1,u'-')
+done = ActionStatus(u'done',2,u'+')
+tickled = ActionStatus(u'tickled',3,u'/')
+inactive = ActionStatus(u'inactive',4,u'!')
+someday = ActionStatus(u'someday',5,u'~')
+info = ActionStatus(u'info',0)
+
+action_regexp = re.compile('(?P<status>[+-/!])?\s*(?P<context>\S*)\s*(?P<description>[^\(]*)(\((?P<info>[^\)]*))?',re.U)
+context_regexp = re.compile('(?P<numbers>\d*)(?P<text>\D?.*)',re.U)
+
+
+def parse_action_line(string):
+ matching = action_regexp.match(string)
+ description = matching.group('description').rstrip(u' \r\n')
+ status_symbol = matching.group('status')
+ if (status_symbol == None):
+ status_symbol = u''
+ status = ActionStatus.get_status(status_symbol)
+ info = matching.group('info')
+ context = parse_context(matching.group('context'))
+ if(info==None):
+ info=u''
+ return context,description,info,status
+
+
+def parse_context(context):
+ context_matching = context_regexp.match(context)
+ context_numbers = context_matching.group('numbers')
+ context_text = context_matching.group('text')
+ if(context_numbers in config.ABBREVIATIONS):
+ context=(unicode(config.ABBREVIATIONS[context_numbers])+context_text).rstrip(u'/')
+ else:
+ context=context_text
+ if (len(context)<2):
+ context = u'None'
+ return context
+
+
+
+
+class Action(ObservableItem,ItemWithStatus):
+ def parse(string):
+ assert type(string) == unicode
+ context,description,info,status = parse_action_line(string)
+ return Action(description,context,info=info,status=status)
+
+ parse = staticmethod(parse)
+
+ def __init__(self,description,context,project=None,info=u'',status=unprocessed):
+ super(Action, self).__init__()
+ self.project = project
+ assert type(description) == unicode
+ assert type(context) == unicode
+ assert type(info) == unicode
+
+ self.description = description
+ self.context = context
+ self.info = info
+ self.status = status
+
+ def is_active(self):
+ return self.status in [active,unprocessed]
+
+ def is_reviewable(self):
+ return self.status in [unprocessed,inactive]
+
+ def is_not_done(self):
+ return self.status in [active,unprocessed,inactive]
+
+ def __repr__(self):
+ advanced_info = u''
+ if self.project:
+ advanced_info = advanced_info+' Project: '+str(self.project)
+ if len(self.info) > 0:
+ advanced_info = advanced_info +' Info: '+self.info
+ if len(advanced_info) > 0:
+ advanced_info = ' ('+advanced_info+' )'
+ return repr(self.description)+' @'+repr(self.context)+repr(advanced_info)
+
+ def project_file_string(self,entry_separator=' '):
+ return (u'%s%s'%(self.status_symbol(),self.context_description_info())).strip()
+
+ def context_description_info(self,entry_separator=' '):
+ return u'%s%s%s%s%s'%(\
+ self.context,entry_separator,\
+ self.description,entry_separator,\
+ self.info_string())
+
+ def info_string(self,entry_separator=''):
+ info_string = u''
+ if (len(self.info) > 1):
+ info_string = u'%s(%s)'%(entry_separator,self.info)
+ return info_string
+
+ def __str__(self):
+ return self.project_file_string()
+ def __cmp__(self,other):
+ return self.status.__cmp__(other.status)
+
+ def summary(self):
+ return self.description
+__all__ = ["Action","ActionStatus","active","done","tickled","inactive","someday","info","unprocessed","parse_action_line","parse_context"]
--- /dev/null
+import time as t
+import calendar
+from types import InstanceType
+
+MINYEAR = 1
+MAXYEAR = 9999
+
+class DaysInMonth:
+ def calculate(self, year, month):
+ return [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] + (month == 2 and self.isleap(year))
+
+ def isleap(self, year):
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
+
+ def test(self):
+ passed = True
+ for year in range (1,9999):
+ for month in range(1,12):
+ if DaysInMonth().calculate(year, month) <> calendar.monthrange(year, month)[1]:
+ print "Failed on %s-%s."%(year, month)
+ passed = False
+ if not passed:
+ print "FAILED"
+ else:
+ print "PASSED"
+ return passed
+
+ def weekday(self, year, month, day):
+ secs = mktime((year, month, day, 0, 0, 0, 0, 0, 0))
+ tuple = localtime(secs)
+ return tuple[6]
+
+
+
+daysInMonth = DaysInMonth()
+
+class datetime:
+ def __init__(self,year,month,day,hour=0,minute=0,second=0,microsecond=0):
+ dates = ['year','month','day','hour','minute']
+ counter = 0
+ for item in [year,month,day,hour,minute]:
+ if type(item) not in [type(1), type(1L)]:
+ raise TypeError("The variable '%s' should be an integer."%dates[counter])
+ counter += 1
+
+ if type(second) not in [type(1), type(1L)]:# and type(second) <> type(1.004):
+ raise ValueError("The variable 'second' should be an Integer or a Long.")# or a float.")
+
+ # Very basic error checking and initialisation.
+ if year < MINYEAR or year > MAXYEAR:
+ raise ValueError('The year value must be between %s and %s inclusive.'%(MINYEAR, MAXYEAR))
+ else:
+ self.year = year
+ if month < 1 or month > 12:
+ raise ValueError('The month value must be between 1 and 12 inclusive.')
+ else:
+ self.month = month
+ if day < 1 or day > daysInMonth.calculate(year, month):
+ raise ValueError('The day value must be between 1 and %s inclusive.'%daysInMonth.calculate(year, month))
+ else:
+ self.day = day
+ if hour < 0 or hour > 23:
+ raise ValueError('The hour value must be between 0 and 23 inclusive.')
+ else:
+ self.hour = hour
+ if minute < 0 or minute > 59:
+ raise ValueError('The minutes value must be between 0 and 59 inclusive.')
+ else:
+ self.minute = minute
+ if second < 0 or second > 59:
+ raise ValueError('The seconds value must be between 0 and 59 inclusive.')
+ else:
+ self.second = second
+ if microsecond < 0 or microsecond > 1000000:
+ raise ValueError('The microseconds value must be between 0 and 1000000 inclusive.')
+ else:
+ self.microsecond = microsecond
+
+ def now(self=None):
+ now = t.localtime()
+ return datetime(now[0],now[1],now[2],now[3],now[4],now[5])
+ now = staticmethod(now)
+
+#
+# Comparison Operators.
+#
+
+ def _compareDate(self, other):
+ if self.year == other.year:
+ if self.month == other.month:
+ if self.day == other.day:
+ return 0
+ elif self.day > other.day:
+ return 1
+ else:
+ return -1
+ elif self.month > other.month:
+ return 1
+ else:
+ return -1
+ elif self.year > other.year:
+ return 1
+ else:
+ return -1
+
+ def _compareTime(self, other):
+ if self.hour == other.hour:
+ if self.minute == other.minute:
+ if self.second == other.second:
+ return 0
+ elif self.second > other.second:
+ return 1
+ else:
+ return -1
+ elif self.minute > other.minute:
+ return 1
+ else:
+ return -1
+ elif self.hour > other.hour:
+ return 1
+ else:
+ return -1
+
+
+ def __cmp__(self, other):
+ if type(other) is type(None):
+ raise Exception('Comparison of %s (%s) with %s (%s) is not supported'%(self,type(self),other,type(other)))
+ elif type(other) is InstanceType:
+# if other.__class__.__name__ == self.__class__.__name__:
+ if other.__class__.__name__ == 'date':
+ return self._compareDate(other)
+ elif other.__class__.__name__ == 'time':
+ return self._compareTime(other)
+ elif other.__class__.__name__ == 'datetime':
+ date = self._compareDate(other)
+ if date == 0:
+ return self._compareTime(other)
+ else:
+ return date
+ else:
+ raise Exception('Comparison of %s (%s) with %s (%s) is not supported'%(self,type(self),other,type(other)))
+# else:
+# raise Exception('Comparison of %s (%s) with %s (%s) is not supported'%(self,self.__class__,other,other.__class__))
+ else:
+ raise Exception('Comparison of %s (%s) with %s (%s) is not supported'%(self,type(self),other,type(other)))
+
+ def __eq__(self, other):
+ if type(other) is InstanceType:
+ if other.__class__.__name__ == self.__class__.__name__:
+ if other.__class__.__name__ == 'date':
+ if self._compareDate(other) == 0:
+ return 1
+ else:
+ return 0
+ elif other.__class__.__name__ == 'time':
+ if self._compareTime(other) == 0:
+ return 1
+ else:
+ return 0
+ elif other.__class__.__name__ == 'datetime':
+ date = self._compareDate(other)
+ if date == 0:
+ if self._compareTime(other) == 0:
+ return 1
+ else:
+ return 0
+ else:
+ return 0
+ else:
+ return 0
+ else:
+ return 0
+ else:
+ return 0
+
+ def __ne__(self, other):
+ if self.__eq__(other):
+ return 0
+ else:
+ return 1
+
+ def __str__(self):
+ return self.isoformat()
+
+ def __repr__(self):
+ return "datetime.datetime(%s,%s,%s,%s,%s,%s)"%(self.year, self.month, self.day, self.hour, self.minute, self.second)
+
+ def __getitem__(self, item):
+ if item == 'year':
+ return self.year
+ elif item == 'month':
+ return self.month
+ elif item == 'day':
+ return self.day
+ elif item == 'hour':
+ return self.hour
+ elif item == 'minute':
+ return self.minute
+ elif item == 'second':
+ return self.second
+ else:
+ raise KeyError("'%s' is not a valid attribute for a Date class."%item)
+
+#
+# Formatting
+#
+
+ def _addZeros(self,num,s):
+ s = str(s)
+ while( len(s) < num ):
+ s = '0'+s
+ return s
+
+ def _isodate(self):
+ return str(self._addZeros(4,self.year))+"-"+str(self._addZeros(2,self.month))+"-"+str(self._addZeros(2,self.day))
+
+ def _isotime(self):
+ return str(self._addZeros(2,self.hour))+":"+str(self._addZeros(2,self.minute))+":"+str(self._addZeros(2,self.second))#str(self._addZeros(2,int(self.second)))+'.'+s
+
+ def strftime(self, format):
+ #raise Exception(self.timetuple())
+ return t.strftime(format, self.timetuple())
+
+
+
+#
+# Conversion
+#
+
+ def timetuple(self):
+ sql = self.isoformat()
+ wday = calendar.weekday(int(sql[0:4]),int(sql[5:7]),int(sql[8:10]))
+ return (int(sql[0:4]),int(sql[5:7]),int(sql[8:10]),int(sql[11:13]),int(sql[14:16]),int(sql[17:19]),wday,0,-1)#,0,0,-1) ,0,0,-1)
+
+ def isoformat(self):
+ return self._isodate() + ' ' + self._isotime()
+
+
+class date(datetime):
+ def __init__(self,year,month,day):
+
+ dates = ['year','month','day']
+ counter = 0
+ for item in [year,month,day]:
+ if type(item) not in [ type(1), type(1L)]:
+ raise TypeError("The variable '%s' should be an Integer or a Long."%dates[counter])
+ counter += 1
+
+
+ # Very basic error checking and initialisation.
+ if year < MINYEAR or year > MAXYEAR:
+ raise ValueError('The year value must be between %s and %s inclusive.'%(MINYEAR, MAXYEAR))
+ else:
+ self.year = year
+ if month < 1 or month > 12:
+ raise ValueError('The month value must be between 1 and 12 inclusive.')
+ else:
+ self.month = month
+ if day < 1 or day > daysInMonth.calculate(year, month):
+ raise ValueError('The day value must be between 1 and %s inclusive.'%daysInMonth.calculate(year, month))
+ else:
+ self.day = day
+
+ def __repr__(self):
+ return "datetime.date(%s,%s,%s)"%(self.year,self.month, self.day)
+
+ def isoformat(self):
+ return self._isodate()
+
+ def timetuple(self):
+ sql = self.isoformat()
+ wday = calendar.weekday(int(sql[0:4]),int(sql[5:7]),int(sql[8:10]))
+ return (int(sql[0:4]),int(sql[5:7]),int(sql[8:10]),0,0,0,wday,0,-1)
+
+ def now(self=None):
+ now = t.localtime()
+ return date(now[0],now[1],now[2])
+ now = staticmethod(now)
+
+ def in_x_days(number_of_days=0):
+ secs = t.mktime(t.localtime())
+ day_secs = secs+24*60*60*number_of_days
+ day = t.localtime(day_secs)
+ return date(day[0],day[1],day[2])
+ in_x_days=staticmethod(in_x_days)
+
+ def tomorrow():
+ return date.in_x_days(1)
+ tomorrow = staticmethod(tomorrow)
+
+class time(datetime):
+
+ def __init__(self,hour=0,minute=0,second=0,microsecond=0):
+
+ dates = ['hour','minute']
+ counter = 0
+ for item in [hour,minute]:
+ if type(item) not in [type(1), type(1L)]:
+ raise TypeError("The variable '%s' should be an Integer or a Long."%dates[counter])
+ counter += 1
+
+ if type(second) <> type(1):# and type(second) <> type(1.004):
+ raise ValueError("The variable 'second' should be an integer.")# or a float.")
+
+ # Very basic error checking and initialisation.
+
+ if hour < 0 or hour > 23:
+ raise ValueError('The hour value must be between 0 and 23 inclusive.')
+ else:
+ self.hour = hour
+ if minute < 0 or minute > 59:
+ raise ValueError('The minutes value must be between 0 and 59 inclusive.')
+ else:
+ self.minute = minute
+ if second < 0 or second > 59:
+ raise ValueError('The seconds value must be between 0 and 59 inclusive.')
+ else:
+ self.second = second
+ if microsecond < 0 or microsecond > 1000000:
+ raise ValueError('The microseconds value must be between 0 and 1000000 inclusive.')
+ else:
+ self.microsecond = microsecond
+
+ def __repr__(self):
+ return "datetime.time(%s,%s,%s)"%(self.hour, self.minute, self.second)
+
+ def isoformat(self):
+ return self._isotime()
+
+ def timetuple(self):
+ raise AttributeError('time objects do not have a timetuple method.')
+
+ def now(self):
+ now = t.localtime()
+ return time(now[3],now[4],now[5])
+
--- /dev/null
+class FilteredList(list):
+# def __init__(self,iterable=None):
+# super(FilteredList,self).__init__(iterable)
+
+ def with_property(self,property):
+ result = FilteredList()
+ for item in self:
+ if property(item):
+ result.append(item)
+ return result
+
+
+class StatusFilteredList(FilteredList):
+ def with_status(self,status):
+ return self.with_property(lambda i:i.status == status)
--- /dev/null
+from model import ObservableItem
+class Info(ObservableItem):
+ def __init__(self,text=u''):
+ super(Info,self).__init__()
+ self.text=text
+ def file_string(self):
+ return u'# %s'%self.text
+
+ def __str__(self):
+ return self.text
+ def __repr__(self):
+ return repr(self.text)
+ def __eq__(self,other):
+ return self.text == other.text
+
+ def __neq__(self,other):
+ return not self.__eq__(other)
--- /dev/null
+import os,re,sys
+import config.config
+import inout.io
+from inout.io import write
+
+from time import *
+from observable import *
+from log.logging import logger
+from config.config import *
+from inout.io import *
+from inout import io
+#logger.log(u'new version')
+
+
+
+
+def invert_dictionary(dictionary):
+ return dict([[v,k] for k,v in dictionary.items()])
+
+def no_transition_policy(self,owner):
+ return self
+
+
+
+class Status(object):
+ symbols = {}
+ names = {}
+ def __init__(self,name,value=0,symbol=u'',transition_policy=no_transition_policy):
+ self.name = name
+ self.value = value
+ self.symbol = symbol
+ Status.symbols[symbol] = self
+ Status.names[name] = self
+ self.transition_policy = transition_policy
+
+ def __eq__(self,other):
+# print "Called eq with %s (%s) and %s (%s)"%(repr(self),type(self),repr(other),type(other))
+# if self == other:
+# return True
+ if (not self and other) or (not other and self):
+ return False
+ return self.name == other.name #and self.value == other.value and type(self) == type(other)
+
+ def __cmp__(self,other):
+ if not other:
+ return 1
+ return other.value - self.value
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return "%s %s %s (%s)"%(type(self),self.value, self.name,id(self))
+
+ def symbol(self):
+ return self.symbol
+
+ def get_status(symbol):
+ return Status.symbols[symbol]
+ get_status = staticmethod(get_status)
+
+ def get_status_for_name(name):
+ return Status.names[name]
+ get_status_for_name = staticmethod(get_status_for_name)
+
+ def update(self,owner):
+ return self.transition_policy(self,owner)
+
+
+
+class ItemWithStatus(object):
+ def __init__(self,status):
+ self.status = status
+
+ def status_symbol(self):
+ if self.status.symbol and len(self.status.symbol) > 0:
+ return self.status.symbol + u' '
+ else:
+ return u''
+
+class WriteableItem(ObservableItem):
+ def __init__(self):
+ super(WriteableItem, self).__init__()
+ def write(self):
+ write(self.path(),self.file_string())
+ def move_to(self,directory,old_dir=None):
+ new_file_name = os.path.join(directory,self.file_name())
+ if old_dir == None:
+ old_dir = self.directory()
+ old_file_name = os.path.join(old_dir,self.file_name())
+ try:
+ os.renames(io.os_encode(old_file_name),io.os_encode(new_file_name))
+ ##logger.log(u'Moved %s to %s'%(repr(old_file_name),repr(new_file_name)))
+ #print u'Moved %s to %s'%(repr(old_file_name),repr(new_file_name))
+ logger.log(u'Moved %s to %s'%(repr(old_file_name),repr(new_file_name)))
+ return new_file_name
+ except OSError,e:
+ logger.log(u'Cannot move %s to %s: %s'%(repr(old_file_name),repr(new_file_name),e.strerror))
+ raise e
+
+
+ def directory(self):
+ return io.os_decode(os.path.dirname(self.encoded_path()))
+ def file_name(self):
+ return io.os_decode(os.path.basename(self.encoded_path()))
+ def remove(self,path=None):
+ if not path:
+ encoded_path = self.encoded_path()
+ else:
+ encoded_path = io.os_encode(path)
+ if os.path.isfile(encoded_path):
+ os.remove(encoded_path)
+ def exists(self):
+ return os.path.isfile(self.encoded_path())
+
+ def encoded_path(self):
+ return io.os_encode(self.path())
+
+ def extension(self):
+ return os.path.splitext(self.encoded_path())[1]
+ def rename(self,new_name,old=None):
+ if not old:
+ old = io.os_decode(os.path.splitext(os.path.basename(self.encoded_path()))[0])
+ directory = io.os_encode(self.directory())
+ extension = io.os_encode(self.extension())
+ old_filename_encoded = io.os_encode(old)
+ new_file_name = os.path.join(directory,io.os_encode(new_name)+extension)
+ old_file_name = os.path.join(directory,old_filename_encoded+extension)
+ #logger.log(u'Renaming %s to %s'%(old_file_name,new_file_name))
+ os.renames(old_file_name,new_file_name)
+
+ def notify(self,action,attribute,new=None,old=None):
+ self.write()
+
+# Public API
+__all__= (
+ 'WriteableItem',
+ 'ItemWithStatus',
+ 'ObservableItem',
+ 'invert_dictionary',
+ 'Status'
+
+ )
--- /dev/null
+
+class ObservableItem(object):
+
+ def __init__(self):
+ self.observers = []
+ def __setattr__(self,name,new_value):
+ #print 'Setting %s to %s'%(name,value)
+ old_value = getattr(self,name,None)
+ super(ObservableItem,self).__setattr__(name,new_value)
+ self.notify_observers(name,new=new_value, old=old_value)
+
+ def notify_observers(self,name,new=None,old=None):
+ if 'observers' in self.__dict__:
+ for observer in self.observers:
+ observer.notify(self,name,new=new,old=old)
+
+
+
+class ObservableList(list,ObservableItem):
+ def __init__(self):
+ ObservableItem.__init__(self)
+
+ def append(self,item):
+ super(ObservableList,self).append(item)
+ self.notify_observers('add_item', item, None)
+
+ def remove(self,item):
+ super(ObservableList,self).remove(item)
+ self.notify_observers('remove_item', item, None)
+
+__all__ = ["ObservableItem","ObservableList"]
--- /dev/null
+from action import Action
+from info import Info
+from model import *
+from datetime import date
+import action
+from observable import *
+from filtered_list import FilteredList,StatusFilteredList
+import datetime
+from log.logging import logger
+import sys
+
+class ProjectStatus(Status):
+ pass
+
+# def __eq__(self,other):
+# return False
+class Inactive(ProjectStatus):
+ def __init__(self):
+ super(Inactive,self).__init__(u'review',4,'!')
+
+ def update(self,project):
+# if project.has_active_actions():
+# #print repr(active)
+# return active
+ return self
+
+
+
+class Active(ProjectStatus):
+
+ def __init__(self):
+ super(Active,self).__init__(u'active',1)
+
+ def update(self,project):
+ if not project.has_active_actions():
+ #print repr(inactive)
+ return inactive
+ return self
+
+
+class Tickled(ProjectStatus):
+ def __init__(self,date=date.tomorrow()):
+ super(Tickled,self).__init__(u'tickled',3,'/')
+ self.date = date
+
+ def update(self,project):
+ if self.date <= date.now():
+ return active
+ else:
+ return self
+
+ def __str__(self):
+ return super(Tickled,self).__str__()+" for %s"%self.date
+
+ def __repr__(self):
+ return self.__str__()
+
+unprocessed = ProjectStatus(u'unprocessed',0)
+active = Active()
+done = ProjectStatus(u'done',2,'+')
+tickled = Tickled()
+inactive = Inactive()
+someday = ProjectStatus(u'someday',5,'~')
+info = ProjectStatus(u'info',0)
+
+
+
+class Project(ObservableItem,ItemWithStatus):
+ observers = []
+ def __init__(self,name,status = inactive):
+ assert type(name) == unicode
+ logger.log(u'Creating project %s (%s)'%(name,status))
+ ItemWithStatus.__init__(self,status)
+ self.name=name
+ self.actions=StatusFilteredList([])
+ self.infos=FilteredList([])
+ self.update_methods = {'status':self.action_changed_status,
+ 'description':self.action_changed_content,
+ 'info':self.action_changed_content,
+ 'context':self.action_changed_content,
+ 'text':self.info_changed}
+ super(Project,self).__init__()
+ for o in Project.observers:
+ o.notify(self.__class__,'new_project',self,None)
+ logger.log(u'Now, its project %s (%s)'%(name,status))
+
+
+ def add_action(self,a):
+ a.project = self
+ a.observers.append(self)
+ self.actions.append(a)
+ self.notify_observers('add_action',a)
+ if a.status == action.unprocessed:
+ a.status = action.active
+
+ def remove_action(self,a):
+ a.status = action.done
+ a.observers.remove(self)
+ self.actions.remove(a)
+ self.notify_observers('remove_action',a)
+
+ def add_info(self,info,position=None):
+ info.observers.append(self)
+ self.infos.append(info)
+ self.notify_observers('add_info', info)
+
+ def remove_info(self,info):
+ info.observers.remove(self)
+ self.infos.remove(info)
+ self.notify_observers('remove_info', info)
+
+ def activate(self):
+ self.status = active
+ for a in self.actions_with_status(action.inactive):
+ a.status = action.active
+
+ def deactivate(self):
+ self.status = inactive
+ for a in self.actions_with_status(action.active):
+ a.status = action.inactive
+
+ def actions_with_status(self,status):
+ return self.actions.with_property(lambda a:a.status == status)
+
+ def active_actions(self):
+ return self.actions_with_status(action.active)
+
+ def has_active_actions(self):
+ return len(self.active_actions()) > 0
+
+ def notify(self,action,attribute,new=None,old=None):
+ self.update_methods[attribute](action,new)
+
+ def info_changed(self,info,text):
+ self.notify_observers('changed_info', info)
+
+ def action_changed_content(self,action,content):
+ self.notify_observers('changed_action',action)
+
+ def action_changed_status(self,a,status):
+ self.notify_observers('changed_action', new=a, old=None)
+
+ def last_modification_date(self):
+ return datetime.date.now()
+
+ def __eq__(self, other):
+ return self.name == other.name and self.status == other.status
+
+ def __ne__(self,project):
+ return not self.__eq__(project)
+
+ def __str__(self):
+ return self.name
+
+ def status_symbol_and_name(self):
+ return self.status_symbol()+self.name
+ def __repr__(self):
+ return u'Project %s (@%s, %s actions, %s infos)'%("Moeject",self.status.name.capitalize(),len(self.actions),len(self.infos))
--- /dev/null
+from project import Project
+from observable import ObservableList
+from filtered_list import FilteredList
+#from tickler import TickleDirectory
+#from inout.io import *
+#
+#project_directory = '@Projects/'
+
+#
+#def make_string_stripper(to_strip):
+# return lambda x: x.replace(to_strip,'')
+
+class Projects(ObservableList,FilteredList):
+ def __init__(self):
+ super(Projects,self).__init__()
+ def with_status(self,status):
+ pass
+ return self.with_property(lambda p:p.status == status)
+
+ def sorted_by_status(self):
+ return sorted(self,cmp=lambda x,y:y.status.__cmp__(x.status))
+# def __init__(self,project_directory):
+# self.review_directory = project_directory+'@Review/'
+# self.done_directory = project_directory+'@Done/'
+# self.someday_directory = project_directory+'@Someday/'
+# self.tickled_directory = project_directory+'@Tickled/'
+# self.project_dir_name = '@Projects/'
+#
+# self.tickle_times=None
+# self.someday_contexts=None
+#
+# self.root = project_directory
+# self.processed_projects = []
+# self.review_projects = []
+# self.someday_projects = []
+# self.tickled_projects = []
+# self.observers = []
+#
+# def get_tickle_times(self):
+# if self.tickle_times == None:
+# self.tickle_times=map(make_string_stripper(self.tickled_directory+'/'),list_dir(self.tickled_directory,True,is_dir))
+# return self.tickle_times
+# def get_someday_contexts(self):
+# if self.someday_contexts == None:
+# self.someday_contexts=map(make_string_stripper(self.someday_directory+'/'),list_dir(self.someday_directory,True,is_dir))
+# return self.someday_contexts
+# #self.notify()
+# def read(self,root,recursive=False):
+# # TODO Use generic read funct
+# return [Project(project_name) for project_name in list_dir(root, recursive, lambda name: name.endswith('.prj'))]
+# def get_all_projects(self):
+# return self.get_active_projects() + self.get_review_projects() + \
+# self.get_tickled_projects() + self.get_someday_projects()
+# def get_current_projects(self):
+# return self.get_active_projects() + self.get_review_projects()
+# def get_inactive_projects(self):
+# return self.get_tickled_projects() + self.get_someday_projects()
+# def get_active_projects(self):
+# if self.processed_projects == None:
+# self.processed_projects = self.read(self.root)
+# return self.processed_projects
+# def get_review_projects(self):
+# if self.review_projects == None:
+# self.review_projects = self.read(self.review_directory)
+# return self.review_projects
+# def get_tickled_projects(self):
+# if self.tickled_projects == None:
+# self.tickled_projects = self.read(self.tickled_directory,True)
+# return self.tickled_projects
+# def get_someday_projects(self):
+# if self.someday_projects == None:
+# self.someday_projects = self.read(self.someday_directory,True)
+# return self.someday_projects
+# def get_current_tickled_projects(self):
+# current_tickled_projects = []
+# tickled_projects = self.get_tickled_projects()
+# for project in tickled_projects:
+# if project.should_not_be_tickled():
+# current_tickled_projects.append(project)
+# return current_tickled_projects
+#
+# def add_project(self,project):
+# # Projects are not being reread
+# if self.processed_projects:
+# self.get_active_projects().insert(0,project)
+# def create_project(self,project_name):
+# project_file_name = (project_directory+project_name+'.prj')
+# project = Project(project_file_name)
+# project.dirty=True
+# project.write()
+# self.add_project(project)
+# return project
+#
+#
+# def process(self):
+# ##logger.log(u'Starting to process')
+#
+# self.reread()
+# ##logger.log('Searching for projects without next act')
+# for project in self.get_active_projects():
+# #logger.log(project.name(),2)
+# self.process_project(project)
+# ##logger.log('Searching for projects that should be untickled')
+# for project in self.get_current_tickled_projects():
+# self.review(project)
+# project.activate()
+# ##logger.log('Removing obsolete tickle directories')
+# for tickle_dir in self.get_tickle_times():
+# if TickleDirectory(tickle_dir).is_obsolete():
+# try:
+# os.removedirs(u'%s/%s'%(self.tickled_directory,tickle_dir))
+# except OSError:
+# pass
+#
+# self.reread()
+# self.notify()
+#
+# def process_project(self,project):
+# is_active = project.process()
+# if not is_active:
+# self.review(project)
+#
+# def update_status(self,project):
+# if project.has_active_actions() and project.get_status()==inactive:
+# self.activate(project)
+# elif not project.has_active_actions() and project.is_processed():
+# self.review(project)
+#
+# def review(self,project):
+# project.move_to(self.review_directory)
+# def set_done(self,project):
+# project.inactivate()
+# project.move_to(self.done_directory)
+# def activate(self,project):
+# project.move_to(self.root)
+#
+# def defer(self,project,context=''):
+# project.inactivate()
+# project.move_to(u'%s/%s'%(self.someday_directory,context))
+# def tickle(self,project,time=''):
+# project.inactivate()
+# project.move_to(u'%s/%s'%(self.tickled_directory,time))
+
+__all__ = ["Projects"]
--- /dev/null
+import re
+date_regexp = re.compile('(?P<number>\d{1,2})(\D.*)?\Z',re.U)
+class TickleDirectory:
+
+ def __init__(self,path):
+ self.path = path
+ def is_obsolete(self):
+ import datetime
+ date = self.date()
+ if not date:
+ # This directory is not a month-day directory
+ return False
+ obsolete = date <= datetime.datetime.now()
+ return obsolete
+ def date(self):
+ import datetime
+ spp = self.path.split(u'/')
+ year = datetime.datetime.now().year
+ try:
+ if len(spp) < 2 or len(spp[0]) == 0:
+ month_match = date_regexp.match(spp[-1])
+ if month_match == None:
+ return None
+ month = int(month_match.group('number').rstrip(u' \r\n'))
+ day = 1
+ else:
+ month_match = date_regexp.match(spp[-2])
+ day_match = date_regexp.match(spp[-1])
+ if month_match == None or day_match == None:
+ return None
+ month = int(month_match.group('number').rstrip(u' \r\n'))
+ day = int(day_match.group('number').rstrip(u' \r\n'))
+
+ if len(spp) > 2:
+ try:
+ year = int(spp[-3].rstrip(u' \r\n'))
+ except:
+ pass
+
+
+ except ValueError:
+ logger.log(repr(spp))
+ return datetime.datetime(year,month,day)
--- /dev/null
+import re,os
+from model.action import *
+from model.model import WriteableItem
+from model import action
+
+class ActiveActionStatus(ActionStatus):
+ def __init__(self):
+ super(ActiveActionStatus,self).__init__('active',1,u'-')
+ def update(self,a):
+ for o in a.observers:
+ if type(o) == ActionFile:
+ if not o.exists():
+ return action.done
+ return self
+
+active_status = ActiveActionStatus()
+action.active = active_status
+
+
+
+class ActionFile(WriteableItem):
+ def __init__(self,action):
+ self.action = action
+ self.update_methods = {'status':self.update_status,'description':self.update_description,'context':self.set_context}
+ self.action.observers.append(self)
+
+ def notify(self,action,attribute,new=None,old=None):
+ super(ActionFile,self).notify(action,attribute,new=new,old=old)
+ if attribute in self.update_methods:
+ self.update_methods[attribute](new=new,old=old)
+
+ def write(self):
+ if self.action.status == active:
+ super(ActionFile,self).write()
+
+ def update_description(self,new,old=None):
+ if self.action.status == active and old:
+ self.remove(self.path(description=old))
+
+ def update_status(self,new,old=None):
+ if new == inactive or new == done:
+ self.remove()
+
+ def set_context(self,new,old=None):
+ if self.action.status == active and old:
+ self.remove(self.path(context=old))
+
+ def update_done_status(self):
+ if self.action.status == active and not self.exists():
+ self.action.status = done
+
+ def path(self,context=None,description=None):
+ if not context:
+ context = self.action.context
+ if not description:
+ description = self.action.description
+ return os.path.join(context,description+'.act')
+
+
+ def file_string(self):
+ string = self.action.project_file_string()
+ if self.action.project:
+ string = string+u'\nProject: %s'%self.action.project
+ return string
+
--- /dev/null
+import re,os,traceback
+from inout.io import parse_file_to_line_list
+from inout import io
+from model.project import *
+from model.model import invert_dictionary,WriteableItem
+from action_file import ActionFile
+
+
+
+projects_dir = '@Projects'
+
+def project_name(file_name):
+ encoded_filename = io.os_encode(file_name)
+ return io.os_decode(os.path.splitext(os.path.basename(encoded_filename))[0])
+
+
+
+
+def status_for_dir(dir_name):
+ p,last_part_of_path = os.path.split(dir_name)
+# print last_part_of_path
+ if last_part_of_path == '.' or last_part_of_path == '@Projects':
+ return active
+ if last_part_of_path[0] == '@':
+ return ProjectStatus.get_status_for_name(last_part_of_path[1:].lower())
+ raise "Invalid path"
+
+def read(file_path):
+ file_name = os.path.basename(file_path)
+ name = project_name(file_name)
+ project = Project(name,status_for_dir(os.path.dirname(file_path)))
+ file_content = parse_file_to_line_list(file_path)
+ actions,infos = parse_lines(file_content)
+ return project,actions,infos
+
+def parse_lines(lines):
+ actions = []
+ infos = []
+ for line in lines:
+ line = unicode(line)
+ if len(line) < 3:
+ continue
+ elif line[0]=='#':
+ infos.append(Info(line[1:].strip()))
+ else:
+ actions.append(Action.parse(line))
+ return (actions,infos)
+
+
+def append_action_file_observer(a):
+ a.observers.append(ActionFile(a))
+
+
+class ProjectFile(WriteableItem):
+ def __init__(self,project):
+ self.project = project
+# project.observers.append(self)
+# import traceback
+# logger.log('Created ProjectFile from %s'%repr(traceback.extract_stack()))
+
+ def path(self):
+ return self.path_for_status(self.project.status)
+
+ def project_file_name(self):
+ return self.project.name+'.prj'
+
+ def path_for_status(self,status):
+ directory = self.directory_for_status(status)
+ if len(directory) > 0:
+ return os.path.join(directory,self.project_file_name())
+ else:
+ return self.project_file_name()
+
+ def directory_for_status(self,status):
+ status_string = status.name.capitalize()
+ if status_string == 'Tickled':
+ year = ''
+ if status.date.year != date.now().year:
+# year = '%s'%status.date.year
+ year = '2012'
+ month = status.date.strftime('%m %B')
+ day = status.date.strftime('%d %A')
+ path = os.path.join(projects_dir,'@Tickled',year,month,day)
+ return path
+ if status_string != 'Active':
+ return os.path.join(projects_dir,'@'+status_string)
+ return projects_dir
+
+ def notify(self,project,attribute,new=None,old=None):
+ if attribute == 'status':
+ self.move_to(self.directory_for_status(new),self.directory_for_status(old))
+ elif attribute == 'name':
+ self.rename(new)
+ elif attribute == 'add_action':
+ append_action_file_observer(new)
+ super(ProjectFile,self).notify(project,attribute,new=new,old=old)
+ else:
+ super(ProjectFile,self).notify(project,attribute,new=new,old=old)
+
+ def file_string(self):
+ lines = []
+ for info in self.project.infos:
+ lines.append(info.file_string())
+# self.sort_actions()
+ for action in self.project.actions:
+ lines.append(action.project_file_string())
+ return u'\n'.join(lines)
+
+
--- /dev/null
+from inout.io import *
+from model.project import Project
+#from project_file import ProjectFile
+import project_file
+from log.logging import logger
+def is_project(path):
+ return os.path.splitext(path)[1]=='.prj'
+
+
+
+class ProjectsDirectory:
+ def __init__(self,projects):
+ self.projects = projects
+ self.projects.observers.append(self)
+ self.projects.reread = self.read
+ self.directories = {}
+ self.num_elements = 0
+
+ def add_directory(self,directory,recursive = False):
+ self.directories[directory] = recursive
+
+ def read(self):
+ for p in range(0,self.num_elements):
+ self.projects.pop()
+ self.num_elements -= 1
+ for directory,recursive in self.directories.items():
+ self.read_directory(directory,recursive)
+
+ def read_directory(self,directory,recursive=False):
+ for f in list_dir(directory,recursive=recursive,filter=is_project):
+
+ p,actions,infos = project_file.read(f)
+# if not p in self.projects:
+ self.projects.append(p)
+ self.num_elements += 1
+ logger.log(u'Read project %s, actions %s, infos %s'%(p,actions,infos))
+ for a in actions:
+ p.add_action(a)
+ for info in infos:
+ p.add_info(info)
+# logger.log(u'Result %s'%repr(p))
+
+ def notify(self,projects,attribute,new=None,old=None):
+ logger.log(repr(type(projects)))
+# p_rep = repr(projects)
+# a_rep = repr(attribute)
+# n_rep = repr(new)
+# logger.log(u"ProjectsDirectory notified of %s | %s | %s"%(p_rep,a_rep,n_rep))
+ if attribute == 'add_item':
+ p_file = project_file.ProjectFile(new)
+ new.observers.append(p_file)
+ p_file.write()
+
--- /dev/null
+import unittest
+import logic.review_visitor
+from logic import review_visitor
+import model.project,model.projects
+from model import action
+from model import project
+from model import datetime
+from model.model import *
+from mock import Mock,patch_object,patch
+import random
+
+class ReviewVisitorBehaviour(unittest.TestCase):
+ pass
+
+ def setUp(self):
+ self.positive_property_actions = [self.create_action() for i in range(10)]
+ self.negative_property_actions =[self.create_action() for i in range(9)]
+
+
+ self.project1 = self.create_project()
+ self.project2 = self.create_project()
+
+ self.project1.actions_with_status.return_value = self.positive_property_actions[:7]
+ self.project1.actions = self.project1.actions_with_status()+self.negative_property_actions[:1]
+ self.project2.actions_with_status.return_value = self.positive_property_actions[7:]
+ self.project2.actions = self.project2.actions_with_status()+self.negative_property_actions[1:]
+
+ self.projects = [self.project1,self.project2]
+ self.reviewer = logic.review_visitor.ReviewVisitor()
+
+ def create_project(self):
+ p = Mock()
+ p.status.update.return_value = p.status
+ p.last_modification_date.return_value = datetime.datetime.now()
+ return p
+
+ def create_action(self):
+ a = Mock(spec=action.Action)
+ a.status = action.unprocessed
+ return a
+
+
+ def create_action_with_status(self,status):
+ a = self.create_action()
+ a.status = status
+ return a
+
+ def review(self):
+ self.reviewer.review(self.projects)
+
+
+
+ def test_should_set_all_unprocessed_actions_to_active(self):
+ for a in self.positive_property_actions:
+ a.status = action.unprocessed
+ for a in self.negative_property_actions:
+ a.status = random.choice([action.done,action.inactive])
+ self.review()
+ for a in self.positive_property_actions:
+ self.assertEqual(a.status,action.active)
+ for a in self.negative_property_actions:
+ self.assertNotEqual(a.status,action.active)
+
+
+ def test_should_untickle_projects_with_tickle_date_present_or_past(self):
+ self.project1.status = project.Tickled(datetime.date.now())
+ self.project2.status = project.Tickled(datetime.date(2030, 12, 2))
+ self.review()
+ self.assertEqual(self.project1.status,project.active)
+ self.assertEqual(self.project2.status,project.tickled)
+
+ def test_should_schedule_projects_with_a_certain_period_of_inactivity_for_review(self):
+ self.project1.last_modification_date.return_value = datetime.datetime(2009, 1, 1)
+ self.project2.last_modification_date.return_value = datetime.datetime(2030, 12, 2)
+ project2_previous_status = self.project2.status
+ self.review()
+ self.assertEqual(self.project1.status,project.inactive)
+ self.assertEqual(self.project2.status,project2_previous_status)
--- /dev/null
+import unittest
+import sys,os
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..')))
+print sys.path
+import model.action
+from model import action
+from mock import Mock
+
+
+
+class UnprocessedStatusBehaviour(unittest.TestCase):
+ def setUp(self):
+ self.status = action.unprocessed
+
+ def test_should_return_active_on_update(self):
+ self.assertEqual(self.status.update(None),action.active)
+
+
+
+class ActionBehaviour(unittest.TestCase):
+
+ def setUp(self):
+ self.action = model.action.Action(u'oldey',u'some_context')
+ self.observer = Mock()
+ self.action.observers.append(self.observer)
+
+ def test_should_be_unprocessed_by_default(self):
+ self.assertEqual(self.action.status,action.unprocessed)
+
+ def test_should_have_new_field_value_when_set(self):
+ self.action.description=u'newey'
+ assert self.action.description == u'newey'
+
+ def test_should_notify_observers_when_field_is_changed_externally(self):
+ self.action.description=u'newea'
+ self.observer.notify.assert_called_with(self.action,'description',new='newea',old='oldey')
+
+ def test_should_notify_observers_when_status_changes(self):
+ self.action.status = action.active
+ self.observer.notify.assert_called_with(self.action,'status',new=action.active,old=action.unprocessed)
+
+
+
+class ActionParseBehaviour(unittest.TestCase):
+
+ def setUp(self):
+ self.description = u'some action'
+ self.context = u'context/sub_context'
+ self.info = u'additional stuff'
+ self.status_string = '-'
+ self.action = model.action.Action.parse(u'%s %s %s (%s)'%(self.status_string,self.context,self.description,self.info))
+
+ def test_should_read_the_description_correctly(self):
+ self.assertEqual(self.action.description, self.description)
+
+ def test_should_read_the_context_correctly(self):
+ self.assertEqual(self.action.context, self.context)
+
+ def test_should_read_the_status_correctly(self):
+ self.assertEqual(self.action.status, action.active)
+
+ def test_should_read_the_info_correctly(self):
+ self.assertEqual(self.action.info, self.info)
--- /dev/null
+import unittest
+from mock import Mock
+import random
+from model.filtered_list import FilteredList
+
+class FilteredListBehaviour(unittest.TestCase):
+
+ def setUp(self):
+ self.list = FilteredList([])
+
+ def test_should_return_filtered_lists(self):
+ l = self.list.with_property(lambda x:True)
+ self.assertEqual(type(l),FilteredList)
+
+
+class EmptyFilteredListBehaviour(FilteredListBehaviour):
+
+ def test_should_a_new_empty_filtered_list_on_with(self):
+ l = self.list.with_property(lambda x:True)
+ self.assertEqual(l,[])
+
+class NonEmptyFilteredListBehaviour(FilteredListBehaviour):
+
+ def setUp(self):
+ super(NonEmptyFilteredListBehaviour,self).setUp()
+
+ self.items,self.items_with_property,self.items_without_property = self.create_items(random.randint(0, 20) ,random.randint(0, 20))
+ for item in self.items:
+ self.list.append(item)
+ def property(self):
+ return lambda x:x.has_property(0)
+ def create_items(self,number_of_items_with_property=1,number_of_items_without_property=1):
+ items_with_property = []
+ for i in range(0,number_of_items_with_property):
+ item = Mock()
+ item.has_property.return_value = True
+ items_with_property.append(item)
+
+ items_without_property = []
+ for i in range(0,number_of_items_without_property):
+ item = Mock()
+ item.has_property.return_value = False
+ items_without_property.append(item)
+ items = items_with_property+items_without_property
+ random.shuffle(items)
+ return (items,items_with_property,items_without_property)
+
+ def filter_results(self):
+ return self.list.with_property(self.property())
+
+ def test_should_test_the_property_on_its_items(self):
+ self.filter_results()
+ for item in self.items:
+ item.has_property.assert_called_with(0)
+
+ def test_should_return_all_items_which_fulfill_the_property(self):
+ filtered = self.filter_results()
+ for item in self.items_with_property:
+ self.assertTrue(item in filtered)
+ for item in filtered:
+ self.assertTrue(item in self.items_with_property)
+
+
+ def test_should_return_none_of_the_items_which_dont_fulfill_the_property(self):
+ filtered = self.filter_results()
+ for item in self.items_without_property:
+ self.assertFalse(item in filtered)
+ for item in filtered:
+ self.assertFalse(item in self.items_without_property)
--- /dev/null
+from model import model
+import unittest
+
+class StatusBehaviour(unittest.TestCase):
+ def setUp(self):
+ self.status = model.Status('schmactive',12,u's')
+
+ def test_should_be_equal_to_other_status_with_identical_name_and_value(self):
+ other = model.Status('schmactive',12)
+ self.assertEqual(self.status,other)
--- /dev/null
+import unittest
+from mock import Mock
+from model.observable import ObservableList
+
+class ObservableListBehaviour(unittest.TestCase):
+ def setUp(self):
+ self.list = ObservableList()
+ self.observer = Mock()
+ self.list.observers.append(self.observer)
+
+ def add_item(self):
+ p = Mock()
+ self.list.append(p)
+ return p
+
+ def assert_observed(self,name,new=None,old=None):
+ self.observer.notify.assert_called_with(self.list,name,new=new,old=old)
+
+ def test_should_be_iterable(self):
+ for item in self.list:
+ pass
+
+ def test_should_notify_observers_if_item_was_added(self):
+ p = self.add_item()
+ self.assert_observed('add_item',new=p,old=None)
+
+ def test_should_remember_added_items(self):
+ p = self.add_item()
+ self.assertTrue(p in self.list)
+
+
+
+class EmptyObservableListBehaviour(ObservableListBehaviour):
+
+ def test_should_be_empty(self):
+ self.assertEqual(len(self.list),0)
+
+ def test_should_raise_exception_when_trying_to_remove_a_item(self):
+ self.assertRaises(ValueError,self.list.remove,Mock())
+
+
+
+class NotEmptyObservableListBehaviour(ObservableListBehaviour):
+
+ def setUp(self):
+ super(NotEmptyObservableListBehaviour,self).setUp()
+ self.add_item()
+
+ def test_should_notify_observers_if_item_was_removed(self):
+ i = self.list[0]
+ self.list.remove(i)
+ self.assert_observed('remove_item', i, None)
--- /dev/null
+import unittest
+from mock import Mock
+import model.project
+
+from model.project import Project
+from model import project
+from model import action
+
+
+class ProjectClassBehaviour(unittest.TestCase):
+ def setUp(self):
+ self.p_class = Project
+ self.observer = Mock()
+ self.p_class.observers.append(self.observer)
+
+ def test_should_inform_listeners_of_project_creation(self):
+ p = Project(u'Test')
+ self.assertTrue(self.observer.notify.called)
+
+class ProjectBehaviour(unittest.TestCase):
+
+ def setUp(self):
+ self.name = u'my project'
+ self.status = self.initial_status()
+ self.actions = self.initial_actions()
+ self.infos = self.initial_infos()
+ self.project = model.project.Project(self.name,self.status)
+ for a in self.actions:
+ self.project.add_action(a)
+ for i in self.infos:
+ self.project.add_info(i)
+ self.observer = Mock()
+ self.project.observers.append(self.observer)
+
+ def initial_actions(self):
+ return []
+
+ def create_action(self):
+ a = Mock()
+ a.status = self.action_status()
+ return a
+
+ def action_status(self):
+ return None
+
+ def initial_infos(self):
+ return []
+
+ def initial_status(self):
+ return project.inactive
+
+ def test_should_remember_its_name(self):
+ self.assertEqual(self.project.name,self.name)
+
+ def test_should_notify_observer_of_name_change(self):
+ self.project.name = 'new name'
+ self.assert_observed('name','new name',self.name)
+
+ def test_should_notify_observer_of_status_change(self):
+ old_status = self.project.status
+ self.project.status = project.done
+ self.assert_observed_status(project.done,old_status)
+
+ def create_action_with_status(self,status=action.inactive):
+ a = Mock()
+ a.status=status
+ return a
+ def test_should_register_itself_as_project_for_added_actions(self):
+ a = self.create_action_with_status()
+ self.project.add_action(a)
+ self.assertEqual(a.project,self.project)
+
+ def test_should_register_itself_as_observer_for_added_actions(self):
+ a = self.create_action()
+ self.project.add_action(a)
+ a.observers.append.assert_called_with(self.project)
+
+ def test_should_notify_observer_of_added_actions(self):
+ a = Mock()
+ a.status = action.done
+ self.project.add_action(a)
+ self.assert_observed('add_action', a)
+
+ def test_should_notify_observers_of_action_status_changes(self):
+ for a in self.project.actions:
+ self.project.action_changed_status(a, action.active)
+ self.assert_observed('changed_action', a, None)
+
+ def test_should_set_added_unprocessed_actions_to_active(self):
+ a = Mock()
+ a.status = action.unprocessed
+ self.project.add_action(a)
+ self.assertEqual(a.status,action.active)
+
+ def test_should_be_equal_if_name_and_status_are_identical(self):
+ other = Mock()
+ other.name = self.project.name
+ other.status = self.project.status
+ self.assertTrue(self.project == other)
+ self.assertFalse(self.project != other)
+ other.name = 'other name'
+ self.assertTrue(self.project != other)
+
+ def assert_observed(self,attribute,new=None,old=None):
+ calls = self.observer.notify.call_args_list
+ self.assertTrue(((self.project,attribute),{'new':new,'old':old}) in calls,
+ 'Expected notification from %s concerning the change of %s from %s to %s\n Only got these calls:\n%s'%(repr(self.project),repr(attribute),repr(old),repr(new),repr(calls)))
+
+ def assert_observed_status(self,status,previous_status=None):
+ if not previous_status:
+ previous_status = self.status
+ self.assert_status(status)
+ self.assert_observed('status',status,previous_status)
+
+ def assert_status(self,status):
+ self.assertEqual(self.project.status,status)
+
+class ActiveProjectBehaviour(ProjectBehaviour):
+
+ def initial_actions(self):
+ a = self.create_action_with_status(action.active)
+ return [a]
+
+ def initial_status(self):
+ return project.active
+
+ def test_should_be_active(self):
+ self.assert_status(project.active)
+
+ def test_should_contain_active_actions(self):
+ self.assertTrue(self.project.has_active_actions())
+
+# def test_should_become_inactive_if_no_active_action_remains(self):
+# self.project.status = project.active
+# for a in self.project.actions_with_status(action.active):
+# self.project.remove_action(a)
+# self.assert_observed_status(project.inactive)
+#
+# def test_should_become_inactive_when_active_actions_become_inactive(self):
+# self.project.status = project.active
+# for a in self.project.actions_with_status(action.active):
+# a.status = action.done
+# self.project.notify(a,'status',action.done)
+# self.assert_observed_status(project.inactive)
+
+ def test_should_deactivate_its_active_actions_on_deactivate(self):
+ active_actions = self.project.actions_with_status(action.active)
+ self.project.deactivate()
+ self.assertEqual(self.project.status,project.inactive)
+ for a in active_actions:
+ self.assertEqual(a.status,action.inactive)
+
+
+
+
+class InactiveProjectBehaviour(ProjectBehaviour):
+ def initial_status(self):
+ return project.inactive
+ def test_should_be_inactive(self):
+ self.assert_status(project.inactive)
+
+# def test_should_become_active_if_active_actions_are_added(self):
+# a = Mock()
+# a.status = action.active
+# self.project.add_action(a)
+# self.assertEqual(self.project.status,project.active)
+
+ def test_should_activate_all_inactive_actions_when_activated_itself(self):
+ inactive_actions = self.project.actions_with_status(action.inactive)
+ for a in inactive_actions:
+ self.assertEqual(a.status,action.inactive)
+ self.project.activate()
+ for a in inactive_actions:
+ self.assertEqual(a.status,action.active)
+
+class EmptyProjectBehaviour(InactiveProjectBehaviour):
+
+ def test_should_return_an_empty_list_of_actions(self):
+ self.assertEqual(self.project.actions,[])
+
+ def test_should_return_an_empty_list_of_infos(self):
+ self.assertEqual(self.project.infos,[])
+
+
+class ProjectWithActionsBehaviour(ProjectBehaviour):
+ def setUp(self):
+ self.action = self.create_action()
+ super(ProjectWithActionsBehaviour,self).setUp()
+
+ def initial_actions(self):
+ return [self.action]
+
+ def test_should_contain_all_added_actions(self):
+ self.assertEqual(self.project.actions,self.actions)
+
+ def test_should_forget_removed_actions(self):
+ self.project.remove_action(self.actions[0])
+ self.assertFalse(self.actions[0] in self.project.actions)
+
+ def test_should_remove_itself_as_observer_for_removed_actions(self):
+ self.project.remove_action(self.actions[0])
+ self.actions[0].observers.remove.assert_called_with(self.project)
+
+ def test_should_set_action_to_done_before_removing(self):
+ self.project.remove_action(self.actions[0])
+ self.assertEqual(self.actions[0].status,action.done)
+
+ def test_should_notify_observer_of_removed_actions(self):
+ self.project.remove_action(self.actions[0])
+ self.assert_observed('remove_action',self.actions[0],None)
+
+def test_generator(field):
+ def test_should_notify_observer_of_changes_in_actions(self):
+ self.project.notify(self.actions[0], field, 'new %s'%field)
+ self.assert_observed('changed_action',self.actions[0])
+ return test_should_notify_observer_of_changes_in_actions
+
+for field in ['description','info','context']:
+ no_change_on_action_becoming_active = 'test_should_notify_observer_of_changes_in_action_%s' % field
+ test = test_generator(field)
+ setattr(ProjectWithActionsBehaviour, no_change_on_action_becoming_active, test)
+
+
+class ProjectWithInactiveActionsBehaviour(ProjectWithActionsBehaviour):
+
+ def action_status(self):
+ return action.inactive
+
+# def test_should_become_active_when_inactive_actions_become_active(self):
+# self.actions[0].status = action.active
+# self.project.notify(self.actions[0],'status',action.active)
+# self.assert_observed_status(project.active)
+
+ def test_should_return_the_inactive_action(self):
+ self.assertEqual(self.project.actions_with_status(action.inactive),[self.actions[0]])
+
+ def test_should_return_no_active_action(self):
+ self.assertEqual(self.project.actions_with_status(action.active),[])
+
+
+
+class InactiveProjectWithInactiveActionsBehaviour(ProjectWithInactiveActionsBehaviour,InactiveProjectBehaviour):
+ pass
+
+
+class ProjectWithActiveActionsBehaviour(ProjectWithActionsBehaviour):
+
+ def action_status(self):
+ return action.active
+
+ def test_should_return_the_active_action(self):
+ self.assertEqual(self.project.actions_with_status(action.active),[self.actions[0]])
+
+ def test_should_return_no_inactive_action(self):
+ self.assertEqual(self.project.actions_with_status(action.inactive),[])
+
+
+
+#class ActiveProjectWithActiveActionsBehaviour(ProjectWithActiveActionsBehaviour,ActiveProjectBehaviour):
+
+
+
+
+class ProjectWithInfosBehaviour(ProjectBehaviour):
+
+ def setUp(self):
+ super(ProjectWithInfosBehaviour,self).setUp()
+ self.info = Mock()
+ self.project.add_info(self.info)
+
+ def test_should_contain_all_added_infos(self):
+ self.assertEqual(self.project.infos,[self.info])
+
+ def test_should_really_forget_removed_infos(self):
+ self.project.remove_info(self.info)
+ self.assertFalse(self.info in self.project.infos)
+
+ def test_should_register_itself_as_observer_for_added_infos(self):
+ self.info.observers.append.assert_called_with(self.project)
+
+ def test_should_deregister_itself_as_observer_for_removed_infos(self):
+ self.project.remove_info(self.info)
+ self.info.observers.remove.assert_called_with(self.project)
+
+
+ def test_should_notify_observer_of_removed_infos(self):
+ self.project.remove_info(self.info)
+ self.assert_observed('remove_info',self.info)
+
+ def test_should_notify_observer_of_changes_in_infos(self):
+ self.project.notify(self.info, 'text', 'new text')
+ self.assert_observed('changed_info',self.info)
+
+
+
+
+def generate_no_becoming_active_test(status):
+ def initial_status(self):
+ return status
+ def test_should_not_change_status_if_actions_become_active(self):
+ self.project.notify(self.actions[0], 'status', action.active)
+ self.assertEqual(status,self.project.status)
+ def test_should_not_change_status_if_active_actions_are_added(self):
+ a = Mock()
+ a.status = action.active
+ self.project.add_action(a)
+ self.assertEqual(status,self.project.status)
+ return (initial_status,test_should_not_change_status_if_actions_become_active,test_should_not_change_status_if_active_actions_are_added)
+#
+for status in ['someday','done','tickled']:
+ class_name = '%sProjectBehaviour'%status.capitalize()
+ my_class=type(class_name,(ProjectWithActiveActionsBehaviour,),{})
+ no_change_on_action_becoming_active = 'test_should_not_change_%s_status_if_actions_become_active' % status
+ no_change_on_active_action_added= 'test_should_not_change_%s_status_if_active_actions_are_added' % status
+ initial_status,no_change_on_action_becoming_active_test,no_change_on_active_action_added_test = generate_no_becoming_active_test(getattr(model.project,status))
+
+ setattr(my_class, 'initial_status', initial_status)
+ setattr(my_class, no_change_on_action_becoming_active, no_change_on_action_becoming_active_test)
+ setattr(my_class, no_change_on_active_action_added, no_change_on_active_action_added_test)
+ globals()[class_name]=my_class
+
+
+
+
+
+
--- /dev/null
+import unittest
+from model.projects import Projects
+from mock import Mock
+
+class ProjectsBehaviour(unittest.TestCase):
+ def setUp(self):
+ self.projects = Projects()
+ self.observer = Mock()
+ self.projects.observers.append(self.observer)
+
+ def create_project(self,name=None,status=None):
+ p = Mock()
+ if name != None:
+ p.name = name
+ if status != None:
+ p.status = status
+ return p
+
+ def assert_observed(self,attribute,new=None,old=None):
+ self.observer.notify.assert_called_with(self.projects,attribute,new=new,old=old)
+
+ def test_should_notify_observers_when_project_is_added(self):
+ p = self.create_project()
+ self.projects.append(p)
+ self.assert_observed('add_item',new=p,old=None)
+
+
+class EmptyProjectsBehaviour(ProjectsBehaviour):
+ def test_should_not_contain_any_projects(self):
+ self.assertEqual(self.projects,[])
+
+class NonEmptyProjectsBehaviour(ProjectsBehaviour):
+
+ def setUp(self):
+ super(NonEmptyProjectsBehaviour,self).setUp()
+ self.searched_projects = [self.create_project(status=0),self.create_project(status=0)]
+ for p in self.searched_projects:
+ self.projects.append(p)
+ self.not_searched_projects = [self.create_project(status=1),self.create_project(status=2)]
+ for p in self.not_searched_projects:
+ self.projects.append(p)
+
+ def test_should_remember_all_added_projects(self):
+ for p in self.searched_projects+self.not_searched_projects:
+ self.assertTrue(p in self.projects)
+
+ def test_should_be_able_to_filter_projects_by_status(self):
+ self.assertEqual(self.projects.with_status(0),self.searched_projects)
+
+ def test_should_notify_observers_when_project_is_removed(self):
+ p = self.projects[0]
+ self.projects.remove(p)
+ self.assert_observed('remove_item',new=p,old=None)
--- /dev/null
+import sys,os,unittest
+sys.path.append(os.path.abspath(os.path.join(os.getcwd(),'..','..','..')))
+#print sys.path
+import file_based_spec
+import persistence.action_file
+#from model.model import *
+#from model.action import *
+from model import action
+
+
+
+class ActionFileBasedBehaviour(file_based_spec.FileBasedBehaviour):
+
+ def setUp(self):
+ super(ActionFileBasedBehaviour,self).setUp()
+ self.context = 'context/sub_context'
+ self.description = 'some action'
+ self.action = action.Action(self.description, self.context)
+ self.action_file()
+
+ def action_file(self):
+ self.action_file = persistence.action_file.ActionFile(self.action)
+
+ def path(self):
+ return os.path.join(self.action.context,self.action.description+'.act')
+
+ def test_should_register_as_an_observer_for_the_action(self):
+ self.assertTrue(self.action_file in self.action.observers)
+
+# def test_should_overwrite_active_status_with_own_implementation(self):
+# self.assertEqual(action.active,persistence.action_file.active_status)
+
+class ActiveActionFileBehaviour(ActionFileBasedBehaviour):
+
+ def setUp(self):
+ super(ActiveActionFileBehaviour,self).setUp()
+ self.action.status = action.active
+
+ def test_should_remove_the_file_when_action_is_set_to_done(self):
+ self.action.status = action.done
+ assert not os.path.isfile(self.path())
+
+ def test_should_remove_the_file_when_action_is_set_to_inactive(self):
+ self.action.status = action.inactive
+ assert not os.path.isfile(self.path())
+
+ def test_should_rename_the_file_when_description_is_changed(self):
+ self.action.description = 'other action'
+ assert os.path.isfile(self.path())
+
+ def test_should_move_the_file_when_context_is_changed(self):
+ old_path = self.path()
+ self.action.context = 'other_context'
+ assert os.path.isfile(self.path())
+ self.assertFalse(os.path.isfile(old_path))
+
+ def test_should_write_the_action_description_in_file(self):
+ content = self.file_content()
+ self.assertTrue(len(content) > 0)
+ self.assertEqual(content, '- %s %s'%(self.context,self.description))
+
+ def test_should_write_if_the_info_changed(self):
+ info = 'new info'
+ self.action.info = info
+ content = self.file_content()
+ self.assertTrue(len(content) > 0)
+ self.assertEqual(content, '- %s %s (%s)'%(self.context,self.description,info))
+
+ def test_should_set_action_status_to_done_on_update_if_file_does_not_exist(self):
+ self.action_file.remove()
+ self.assertEqual(self.action.status.update(self.action),action.done)
+
+
+def generate_test_for_write_on_change_notification(field):
+ def test_should_write_if_notified_of_changes(self):
+ d = 'new %s'%field
+ setattr(self.action,field, d)
+ self.action_file.notify(self.action, field, d)
+ self.assertTrue(d in self.file_content())
+ return test_should_write_if_notified_of_changes
+
+for field in ['description','info','context']:
+ test_name = 'test_should_write_when_notified_of_changed_%s' % field
+ test = generate_test_for_write_on_change_notification(field)
+ setattr(ActiveActionFileBehaviour, test_name, test)
+
+
+
+class UnprocessedActionFileBehaviour(ActionFileBasedBehaviour):
+
+ def setUp(self):
+ super(UnprocessedActionFileBehaviour,self).setUp()
+ self.action.status = action.unprocessed
+
+ def test_should_not_have_created_a_file(self):
+ assert not os.path.isfile(self.path())
+
+ def test_should_create_a_file_when_action_is_set_active(self):
+ self.action.status = action.active
+ assert os.path.isfile(self.path())
+
+ def test_should_not_create_the_file_when_description_is_changed(self):
+ self.action.description = 'other action'
+ assert not os.path.isfile(self.path())
+
+ def test_should_move_the_file_when_context_is_changed(self):
+ self.action.context = 'other_context'
+ assert not os.path.isfile(self.path())
+
+def generate_test_for_not_writing_on_change_notification(field):
+ def test_should_not_write_if_notified_of_changes(self):
+ d = 'new %s'%field
+ setattr(self.action,field, d)
+ self.action_file.notify(self.action, field, d)
+ self.assertFalse(os.path.isfile(self.path()))
+ return test_should_not_write_if_notified_of_changes
+
+for field in ['description','info','context']:
+ test_name = 'test_should_not_write_when_notified_of_changed_%s' % field
+ test = generate_test_for_not_writing_on_change_notification(field)
+ setattr(UnprocessedActionFileBehaviour, test_name, test)
+
+
+class ActiveDeletedActionFileBehaviour(ActionFileBasedBehaviour):
+
+ def setUp(self):
+ super(ActiveDeletedActionFileBehaviour,self).setUp()
+ self.action.status = action.active
+# os.remove(self.path())
+ ActionFileBasedBehaviour.action_file(self)
+
+ def action_file(self):
+ pass
+
+
+class DoneActionFileBehaviour(UnprocessedActionFileBehaviour):
+
+ def setUp(self):
+ super(DoneActionFileBehaviour,self).setUp()
+ self.action.status = action.done
+
+
+
+class InactiveActionFileBehaviour(UnprocessedActionFileBehaviour):
+ def setUp(self):
+ super(InactiveActionFileBehaviour,self).setUp()
+ self.action.status = action.inactive
+
--- /dev/null
+import unittest
+import os,tempfile,shutil
+import inout.io
+from inout import io
+
+
+class FileSystemBasedBehaviour(unittest.TestCase):
+ def setUp(self):
+ self.current_dir = os.getcwd()
+ self.tempdir = tempfile.mkdtemp()
+ os.chdir(self.tempdir)
+ def tearDown(self):
+ os.chdir(self.current_dir)
+ #shutil.rmtree(self.tempdir,True)
+
+ def create_file(self,path):
+ inout.io.create_file(path).close()
+
+ def assertCreatedFile(self,path,error_message = None):
+ if not error_message:
+ error_message = u"The file %s should have been created"%repr(path)
+ self.assertTrue(os.path.isfile(io.os_encode(path)),error_message)
+
+class FileBasedBehaviour(FileSystemBasedBehaviour):
+
+ def file_content(self):
+ f=file(self.path(),'r')
+ raw=f.read()
+ f.close()
+ return raw
+
+ def create_file(self,path=None):
+ if path == None:
+ path = self.path()
+ super(FileBasedBehaviour,self).create_file(path)
+
+ def write(self,content,path=None):
+ if path == None:
+ path = self.path()
+ inout.io.write(path,content)
+
+
+__all__= ["FileBasedBehaviour","FileSystemBasedBehaviour"]
--- /dev/null
+# coding: utf-8
+from mock import Mock
+import file_based_spec
+import unittest
+from persistence import project_file
+from persistence.project_file import ProjectFile
+from model import project
+from model import action
+from model import info
+from model import datetime
+from persistence.action_file import ActionFile
+import os
+from inout import io
+
+
+
+
+class ProjectFileBehaviour(file_based_spec.FileBasedBehaviour):
+
+ def setUp(self):
+ super(ProjectFileBehaviour,self).setUp()
+ self.project = self.create_project()
+ self.project_file = ProjectFile(self.project)
+
+ def create_project(self):
+ project = Mock()
+ project.status = self.status()
+ project.name = u'some projectüß'
+ project.actions = self.create_actions()
+ project.observers = []
+ self.info = Mock()
+ self.info.file_string.return_value = 'important info'
+ project.infos = [self.info]
+ return project
+ def create_actions(self):
+ self.action1 = Mock()
+ self.action1.status = action.active
+ self.action1.project_file_string.return_value = 'first action'
+ self.action2 = Mock()
+ self.action2.status = action.inactive
+ self.action2.project_file_string.return_value = 'second action'
+ return [self.action1,self.action2]
+
+ def status(self):
+ return project.active
+
+# def test_should_have_registered_itself_as_observer(self):
+# self.assertTrue(self.project_file in self.project.observers)
+
+ def test_should_calc_file_name_as_project_name_plus_extension(self):
+ self.assertEqual(self.project_file.file_name(),self.project.name+'.prj')
+
+ def path(self):
+ return self.path_in_subdirectory(self.subdir())
+
+ def path_in_subdirectory(self,subdir):
+ project_file_name = self.project.name+'.prj'
+ if subdir and len(subdir) > 0:
+ return os.path.join('@Projects',subdir,project_file_name)
+ else:
+ return os.path.join('@Projects',project_file_name)
+
+ def test_should_create_an_action_file_if_notified_of_added_action(self):
+ a = Mock()
+ a.observers = []
+ a.status = action.inactive
+ self.project_file.notify(self.project, 'add_action', a, None)
+ self.assertTrue(has_added_action_file_as_observer(a))
+
+
+class ProjectFileWithNonAsciiCharacterName(ProjectFileBehaviour):
+ def create_project(self):
+ project = super(ProjectFileWithNonAsciiCharacterName,self).create_project()
+ project.name = u'some project with ümläutß'
+ return project
+
+
+
+class ExistingProjectFileBehaviour:
+
+ def setUp(self):
+ super(ExistingProjectFileBehaviour,self).setUp()
+
+ def assert_moved_file_to_correct_directory_if_status_changes(self,status,subdir):
+ self.create_file()
+ old_status = self.project.status
+ self.project.status = status
+ self.project_file.notify(self.project, 'status', status,old_status)
+ self.assertTrue(os.path.isfile(io.os_encode(self.path_in_subdirectory(subdir))),"Should have moved file to %s"%self.path_in_subdirectory(subdir))
+
+ def test_should_move_file_correctly_to_review_directory(self):
+ self.assert_moved_file_to_correct_directory_if_status_changes(project.inactive,'@Review')
+
+ def test_should_move_file_correctly_to_active_directory(self):
+ self.assert_moved_file_to_correct_directory_if_status_changes(project.active,'')
+
+ def test_should_move_file_correctly_to_someday_directory(self):
+ self.assert_moved_file_to_correct_directory_if_status_changes(project.someday,'@Someday')
+
+ def test_should_move_file_correctly_to_tickled_with_date_directory(self):
+ self.assert_moved_file_to_correct_directory_if_status_changes(project.Tickled(datetime.date(2009,12,31)), os.path.join('@Tickled','12 December','31 Thursday'))
+ def test_should_move_file_correctly_to_tickled_with_date_in_another_year_directory(self):
+ self.assert_moved_file_to_correct_directory_if_status_changes(project.Tickled(datetime.date(2012,12,31)), os.path.join('@Tickled','2012','12 December','31 Monday'))
+ def test_should_rename_file_if_project_name_changes(self):
+ name = 'new name'
+ self.create_file()
+ self.project_file.notify(self.project, 'name', name)
+ self.project.name = name
+ assert os.path.isfile(self.path())
+
+ def test_should_calc_path_correctly(self):
+ self.assertEqual(self.project_file.path(),self.path())
+
+ def test_should_write_if_notified_of_changes(self):
+ self.project_file.notify(self.project, 'add_action', Mock(),None)
+ self.assertCreatedFile(self.path())
+
+
+
+
+class WritingProjectFileBehaviour(ExistingProjectFileBehaviour):
+ def test_should_write_the_project_description_in_file(self):
+# pass
+ self.project_file.write()
+ content = self.file_content()
+ assert len(content) > 0
+ assert self.info.file_string() in content
+ assert self.action1.project_file_string() in content
+ assert self.action2.project_file_string() in content
+
+
+
+class ActiveProjectFileBehaviour(ProjectFileBehaviour,ExistingProjectFileBehaviour):
+
+ def status(self):
+ return project.active
+
+ def subdir(self):
+ return ''
+
+
+
+
+class SomedayProjectFileBehaviour(ProjectFileBehaviour,ExistingProjectFileBehaviour):
+
+ def status(self):
+ return project.someday
+
+ def subdir(self):
+ return '@Someday'
+
+
+class InactiveProjectFileBehaviour(ProjectFileBehaviour,ExistingProjectFileBehaviour):
+ def status(self):
+ return project.inactive
+
+ def subdir(self):
+ return '@Review'
+
+
+
+class ProjectFileReaderBehaviour(ProjectFileBehaviour,ExistingProjectFileBehaviour):
+
+# def setUp(self):
+# super(ProjectFileReaderBehaviour,self).setUp()
+# self.project.add_action.side_effect = lambda a:self.project_file.notify(self.project, 'add_action', a, None)
+
+ def create_project(self):
+ self.original_project = self.create_original_project()
+ self.original_project.add_info(info.Info('some info'))
+ active_action = action.Action('active action','Online/Google',status=action.inactive)
+ self.original_project.add_action(active_action)
+ p_file = ProjectFile(self.original_project)
+ self.write(p_file.file_string(),p_file.path())
+# self.original_project.observers.remove(p_file)
+ p,self.actions,self.infos = project_file.read(p_file.path())
+ for a in self.actions:
+ p.add_action(a)
+ for i in self.infos:
+ p.add_info(i)
+ return p
+
+ def create_original_project(self):
+ self.project_name = u'Exämple Project'
+ return project.Project(self.project_name)
+
+ def create_actions(self):
+ return self.actions
+
+ def path(self):
+ return self.project_file.path()
+
+ def test_should_read_the_project_name_correctly(self):
+ self.assertEqual(self.project.name,self.project_name)
+
+ def test_should_infer_the_status_from_the_path(self):
+ self.assertEqual(self.project.status,self.original_project.status)
+
+ def test_should_read_the_infos_correctly(self):
+ self.assertEqual(self.project.infos,[info.Info('some info')])
+
+ def test_should_read_the_actions_correctly(self):
+ a = action.Action('active action','Online/Google',status=action.inactive)
+ a.project = self.project
+ self.assertEqual(self.project.actions,[a])
+
+# def test_should_create_action_files_for_all_actions(self):
+# for a in self.project.actions:
+# self.assertTrue(has_added_action_file_as_observer(a))
+
+class DoneProjectFileReaderBehaviour(ProjectFileReaderBehaviour):
+ def create_original_project(self):
+ p = ProjectFileReaderBehaviour.create_original_project(self)
+ p.status = project.done
+ return p
+
+
+def has_added_action_file_as_observer(a):
+ has_action_file=False
+ for o in a.observers:
+ if type(o) == ActionFile and o.action == a:
+ has_action_file=True
+ return has_action_file
--- /dev/null
+# coding: utf-8
+import unittest
+from mock import Mock,patch_object,patch
+from persistence.projects_directory import ProjectsDirectory
+from file_based_spec import FileSystemBasedBehaviour
+from model.project import Project,active
+from persistence.project_file import ProjectFile
+from persistence import project_file
+from sets import Set
+class ProjectsDirectoryBehaviour(FileSystemBasedBehaviour):
+
+ def setUp(self):
+ super(ProjectsDirectoryBehaviour,self).setUp()
+ self.projects = Mock()
+ self.project_list = []
+ self.projects.__iter__ = self.project_list.__iter__
+ self.projects_directory = ProjectsDirectory(self.projects)
+ self.projects_directory.add_directory('.')
+
+ def create_project_file(self,name,subdir=None):
+ file_name = name+'.prj'
+ if subdir:
+ filename = os.path.join(subdir,file_name)
+ self.create_file(file_name)
+
+ def assert_project_added(self,project_name):
+ calls = self.projects.append.call_args_list
+ self.assertTrue(((Project(project_name,active),),{}) in calls,u"Project %s was not created:\n%s"%(repr(Project(project_name)),calls))
+# self.projects.append.assert_called_with(Project(project_name))
+
+ def test_should_register_itself_as_observer(self):
+ self.projects.observers.append.assert_called_with(self.projects_directory)
+
+ def read(self):
+ self.projects_directory.read()
+
+
+
+class EmptyProjectsDirectoryBehaviour(ProjectsDirectoryBehaviour):
+
+ def test_should_not_read_any_projects(self):
+ self.read()
+ self.assertFalse(self.projects.append.called)
+
+
+
+class NonEmptyProjectsDirectoryBehaviour(ProjectsDirectoryBehaviour):
+
+ def setUp(self):
+ super(NonEmptyProjectsDirectoryBehaviour,self).setUp()
+ self.project_names = Set([u'Fürst Project',u'other project',u'third something'])
+ self.create_project_files(self.project_names)
+
+
+ def create_project_files(self,names):
+ for name in names:
+ self.create_project_file(name)
+
+ def test_should_read_all_projects_in_this_directory(self):
+ self.read()
+ for p in self.project_names:
+ self.assert_project_added(p)
+
+ def test_should_read_only_project_files(self):
+ self.create_file('First something.txt')
+ self.read()
+ self.assertEqual(len(self.projects.append.call_args_list),len(self.project_names))
+ self.assertFalse(((Project(u'First something.txt'),),{}) in self.projects.append.call_args_list)
+
+ def has_project_file(self,p):
+ has_project_file = False
+ for o in p.observers:
+ if type(o) == ProjectFile:
+ if has_project_file:
+ self.fail("Added ProjectFile twice")
+ has_project_file = True
+ return has_project_file
+
+ def test_should_add_read_projects_to_projects(self):
+# self.projects.append.side_effect = self.mock_notify
+ self.read()
+ read_project_names = Set()
+ for call in self.projects.append.call_args_list:
+ read_project_names.add(call[0][0].name)
+ self.assertEqual(read_project_names,self.project_names)
+
+ def test_should_clear_the_projects_before_reading(self):
+ self.read()
+ self.read()
+ self.assertEqual(len(self.projects.pop.call_args_list),len(self.project_names))
+
+ @patch('persistence.project_file.ProjectFile')
+ def test_should_create_project_files_when_notified_of_added_projects(self,project_file_constructor):
+ p = self.create_and_notify_of_new_project()
+ project_file_constructor.assert_called_with(p)
+
+ @patch('persistence.project_file.ProjectFile')
+ def test_should_immediately_write_added_project_files(self,project_file_constructor):
+ self.create_and_notify_of_new_project()
+ project_file_constructor().write.assert_called_with()
+
+ def create_and_notify_of_new_project(self):
+ p = Mock()
+ self.projects_directory.notify(self.projects,'add_item',p, None)
+ return p
+
+ @patch('persistence.project_file.read')
+ def test_should_make_the_project_file_read_the_file_contents(self,read_method):
+ read_method.return_value=(None,[],[])
+ self.read()
+ self.assertEqual(len(read_method.call_args_list),len(self.project_names))
+
--- /dev/null
+from pyspec import *
+
+class VerifyUserSpecification( object ):
+ @context
+ def setUp( self ):
+ self.user = User( "Mark Dancer" )
+ @spec
+ def verifyInitialUserNameIsNameInConstructor( self ):
+ self.shouldBeEqual( self.user.name, "Mark Dancer" )
+
+ def verifyInitialUserHasNoLanguages( self ):
+ self.shouldBeEmpty( self.user.languages )
+
+if __name__ == "__main__":
+ run_test()
--- /dev/null
+import nose
+import os
+from nose.selector import Selector
+from nose.plugins import Plugin
+
+import unittest
+
+
+class MySelector(Selector):
+ def wantDirectory(self, dirname):
+ parts = dirname.split(os.path.sep)
+ return 'specs' in parts
+ def wantFile(self, filepath):
+
+ # we want python modules under specs/
+ dirname,filename = os.path.split(filepath)
+ base, ext = os.path.splitext(filename)
+ return self.wantDirectory(dirname) and ext == '.py' and base[0:2] != '__'
+ def wantModule(self, module):
+ # wantDirectory and wantFile above will ensure that
+ # we never see an unwanted module
+ return True
+ def wantFunction(self, function):
+ # never collect functions
+ return False
+ def wantClass(self, cls):
+ # only collect TestCase subclasses
+ return issubclass(cls, unittest.TestCase)
+
+class UseMySelector(Plugin):
+ enabled = True
+ def configure(self, options, conf):
+ pass # always on
+ def prepareTestLoader(self, loader):
+ loader.selector = MySelector(loader.config)
+nose.main(plugins=[UseMySelector()])