Add files from the old svn, r101.
authorMatthijs Kooijman <matthijs@stdin.nl>
Wed, 12 Aug 2009 16:31:19 +0000 (18:31 +0200)
committerMatthijs Kooijman <matthijs@stdin.nl>
Thu, 13 Aug 2009 09:58:33 +0000 (11:58 +0200)
The directory structure was changed and the files were converted to unix
line endings, but no other changes were made. The contents of the files
is still unmodified. This means it (still) doesn't work currently.

60 files changed:
Changelog.txt [new file with mode: 0644]
build/MobileGTD3rd.bat [new file with mode: 0644]
src/config/__init__.py [new file with mode: 0644]
src/config/config.py [new file with mode: 0644]
src/config/defaultconfig.py [new file with mode: 0644]
src/default.py [new file with mode: 0644]
src/gui/__init__.py [new file with mode: 0644]
src/gui/gui.py [new file with mode: 0644]
src/gui/project_details/__init__.py [new file with mode: 0644]
src/gui/project_details/action_view.py [new file with mode: 0644]
src/gui/project_details/action_widget.py [new file with mode: 0644]
src/gui/project_details/context_widget.py [new file with mode: 0644]
src/gui/project_details/info_widget.py [new file with mode: 0644]
src/gui/project_details/infos_widget.py [new file with mode: 0644]
src/gui/project_details/project_view.py [new file with mode: 0644]
src/gui/projects_list/__init__.py [new file with mode: 0644]
src/gui/projects_list/new_action_widget.py [new file with mode: 0644]
src/gui/projects_list/new_project_widget.py [new file with mode: 0644]
src/gui/projects_list/project_list_view.py [new file with mode: 0644]
src/gui/projects_list/project_widget.py [new file with mode: 0644]
src/gui/projects_list/sms_widget.py [new file with mode: 0644]
src/inout/__init__.py [new file with mode: 0644]
src/inout/io.py [new file with mode: 0644]
src/log/__init__.py [new file with mode: 0644]
src/log/logging.py [new file with mode: 0644]
src/log/traceS60.py [new file with mode: 0644]
src/logic/__init__.py [new file with mode: 0644]
src/logic/review_visitor.py [new file with mode: 0644]
src/main.py [new file with mode: 0644]
src/model/__init__.py [new file with mode: 0644]
src/model/action.py [new file with mode: 0644]
src/model/datetime.py [new file with mode: 0644]
src/model/filtered_list.py [new file with mode: 0644]
src/model/info.py [new file with mode: 0644]
src/model/model.py [new file with mode: 0644]
src/model/observable.py [new file with mode: 0644]
src/model/project.py [new file with mode: 0644]
src/model/projects.py [new file with mode: 0644]
src/model/tickler.py [new file with mode: 0644]
src/persistence/__init__.py [new file with mode: 0644]
src/persistence/action_file.py [new file with mode: 0644]
src/persistence/project_file.py [new file with mode: 0644]
src/persistence/projects_directory.py [new file with mode: 0644]
tests/specs/__init__.py [new file with mode: 0644]
tests/specs/logic/__init__.py [new file with mode: 0644]
tests/specs/logic/review_visitor_spec.py [new file with mode: 0644]
tests/specs/model/__init__.py [new file with mode: 0644]
tests/specs/model/action_spec.py [new file with mode: 0644]
tests/specs/model/filtered_list_spec.py [new file with mode: 0644]
tests/specs/model/model_spec.py [new file with mode: 0644]
tests/specs/model/observable_spec.py [new file with mode: 0644]
tests/specs/model/project_spec.py [new file with mode: 0644]
tests/specs/model/projects_spec.py [new file with mode: 0644]
tests/specs/persistence/__init__.py [new file with mode: 0644]
tests/specs/persistence/action_file_spec.py [new file with mode: 0644]
tests/specs/persistence/file_based_spec.py [new file with mode: 0644]
tests/specs/persistence/project_file_spec.py [new file with mode: 0644]
tests/specs/persistence/projects_directory_spec.py [new file with mode: 0644]
tests/specs/ui_spec.py [new file with mode: 0644]
tests/test_all.py [new file with mode: 0644]

diff --git a/Changelog.txt b/Changelog.txt
new file mode 100644 (file)
index 0000000..f5efc2e
--- /dev/null
@@ -0,0 +1,11 @@
+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
+
diff --git a/build/MobileGTD3rd.bat b/build/MobileGTD3rd.bat
new file mode 100644 (file)
index 0000000..550a839
--- /dev/null
@@ -0,0 +1,15 @@
+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
diff --git a/src/config/__init__.py b/src/config/__init__.py
new file mode 100644 (file)
index 0000000..e296e08
--- /dev/null
@@ -0,0 +1 @@
+__all__ = ["config","defaultconfig"]
diff --git a/src/config/config.py b/src/config/config.py
new file mode 100644 (file)
index 0000000..f0aa0aa
--- /dev/null
@@ -0,0 +1,92 @@
+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"]
diff --git a/src/config/defaultconfig.py b/src/config/defaultconfig.py
new file mode 100644 (file)
index 0000000..0b03243
--- /dev/null
@@ -0,0 +1,50 @@
+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/"
+}
diff --git a/src/default.py b/src/default.py
new file mode 100644 (file)
index 0000000..d291aef
--- /dev/null
@@ -0,0 +1,3 @@
+# SYMBIAN_UID = 0xA0008CDD
+sys.path.append('c:\\private\\a0008cdd')
+import main
diff --git a/src/gui/__init__.py b/src/gui/__init__.py
new file mode 100644 (file)
index 0000000..9c0fc5b
--- /dev/null
@@ -0,0 +1 @@
+__all__ = ["gui"]
diff --git a/src/gui/gui.py b/src/gui/gui.py
new file mode 100644 (file)
index 0000000..ec0a7cb
--- /dev/null
@@ -0,0 +1,277 @@
+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')
diff --git a/src/gui/project_details/__init__.py b/src/gui/project_details/__init__.py
new file mode 100644 (file)
index 0000000..98f136b
--- /dev/null
@@ -0,0 +1 @@
+__all__ = ["gui","project_view"]
diff --git a/src/gui/project_details/action_view.py b/src/gui/project_details/action_view.py
new file mode 100644 (file)
index 0000000..d9197fc
--- /dev/null
@@ -0,0 +1,72 @@
+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])
diff --git a/src/gui/project_details/action_widget.py b/src/gui/project_details/action_widget.py
new file mode 100644 (file)
index 0000000..f43c53b
--- /dev/null
@@ -0,0 +1,20 @@
+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__()
diff --git a/src/gui/project_details/context_widget.py b/src/gui/project_details/context_widget.py
new file mode 100644 (file)
index 0000000..d562671
--- /dev/null
@@ -0,0 +1,12 @@
+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
diff --git a/src/gui/project_details/info_widget.py b/src/gui/project_details/info_widget.py
new file mode 100644 (file)
index 0000000..229a63b
--- /dev/null
@@ -0,0 +1,17 @@
+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
diff --git a/src/gui/project_details/infos_widget.py b/src/gui/project_details/infos_widget.py
new file mode 100644 (file)
index 0000000..dea2368
--- /dev/null
@@ -0,0 +1,13 @@
+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'
diff --git a/src/gui/project_details/project_view.py b/src/gui/project_details/project_view.py
new file mode 100644 (file)
index 0000000..bc36d77
--- /dev/null
@@ -0,0 +1,127 @@
+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')
diff --git a/src/gui/projects_list/__init__.py b/src/gui/projects_list/__init__.py
new file mode 100644 (file)
index 0000000..a4e0b63
--- /dev/null
@@ -0,0 +1 @@
+__all__ = ["project_list_view"]
diff --git a/src/gui/projects_list/new_action_widget.py b/src/gui/projects_list/new_action_widget.py
new file mode 100644 (file)
index 0000000..119a450
--- /dev/null
@@ -0,0 +1,13 @@
+
+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?')
diff --git a/src/gui/projects_list/new_project_widget.py b/src/gui/projects_list/new_project_widget.py
new file mode 100644 (file)
index 0000000..c08a1ac
--- /dev/null
@@ -0,0 +1,25 @@
+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'')
diff --git a/src/gui/projects_list/project_list_view.py b/src/gui/projects_list/project_list_view.py
new file mode 100644 (file)
index 0000000..1288d10
--- /dev/null
@@ -0,0 +1,76 @@
+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()
+
+
+
+
diff --git a/src/gui/projects_list/project_widget.py b/src/gui/projects_list/project_widget.py
new file mode 100644 (file)
index 0000000..e87bbda
--- /dev/null
@@ -0,0 +1,69 @@
+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
diff --git a/src/gui/projects_list/sms_widget.py b/src/gui/projects_list/sms_widget.py
new file mode 100644 (file)
index 0000000..f84632e
--- /dev/null
@@ -0,0 +1,53 @@
+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)
diff --git a/src/inout/__init__.py b/src/inout/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/inout/io.py b/src/inout/io.py
new file mode 100644 (file)
index 0000000..68b6913
--- /dev/null
@@ -0,0 +1,97 @@
+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))
diff --git a/src/log/__init__.py b/src/log/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/log/logging.py b/src/log/logging.py
new file mode 100644 (file)
index 0000000..761a20f
--- /dev/null
@@ -0,0 +1,58 @@
+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()
diff --git a/src/log/traceS60.py b/src/log/traceS60.py
new file mode 100644 (file)
index 0000000..141bba1
--- /dev/null
@@ -0,0 +1,30 @@
+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
+
+
+
diff --git a/src/logic/__init__.py b/src/logic/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/logic/review_visitor.py b/src/logic/review_visitor.py
new file mode 100644 (file)
index 0000000..d29239e
--- /dev/null
@@ -0,0 +1,24 @@
+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()
diff --git a/src/main.py b/src/main.py
new file mode 100644 (file)
index 0000000..80a7538
--- /dev/null
@@ -0,0 +1,88 @@
+# 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()
diff --git a/src/model/__init__.py b/src/model/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/model/action.py b/src/model/action.py
new file mode 100644 (file)
index 0000000..05c2b1c
--- /dev/null
@@ -0,0 +1,118 @@
+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"]
diff --git a/src/model/datetime.py b/src/model/datetime.py
new file mode 100644 (file)
index 0000000..93c9a7a
--- /dev/null
@@ -0,0 +1,336 @@
+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])
+        
diff --git a/src/model/filtered_list.py b/src/model/filtered_list.py
new file mode 100644 (file)
index 0000000..82c0ea0
--- /dev/null
@@ -0,0 +1,15 @@
+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)   
diff --git a/src/model/info.py b/src/model/info.py
new file mode 100644 (file)
index 0000000..0875be5
--- /dev/null
@@ -0,0 +1,17 @@
+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)
diff --git a/src/model/model.py b/src/model/model.py
new file mode 100644 (file)
index 0000000..e6910bd
--- /dev/null
@@ -0,0 +1,143 @@
+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'
+         
+                 )
diff --git a/src/model/observable.py b/src/model/observable.py
new file mode 100644 (file)
index 0000000..e8a0e9b
--- /dev/null
@@ -0,0 +1,31 @@
+
+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"]
diff --git a/src/model/project.py b/src/model/project.py
new file mode 100644 (file)
index 0000000..7e2cf05
--- /dev/null
@@ -0,0 +1,158 @@
+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))
diff --git a/src/model/projects.py b/src/model/projects.py
new file mode 100644 (file)
index 0000000..c20d5fe
--- /dev/null
@@ -0,0 +1,144 @@
+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"]
diff --git a/src/model/tickler.py b/src/model/tickler.py
new file mode 100644 (file)
index 0000000..021b0cf
--- /dev/null
@@ -0,0 +1,43 @@
+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)
diff --git a/src/persistence/__init__.py b/src/persistence/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/persistence/action_file.py b/src/persistence/action_file.py
new file mode 100644 (file)
index 0000000..547aa83
--- /dev/null
@@ -0,0 +1,65 @@
+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
+
diff --git a/src/persistence/project_file.py b/src/persistence/project_file.py
new file mode 100644 (file)
index 0000000..360d7ce
--- /dev/null
@@ -0,0 +1,109 @@
+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) 
+
+    
diff --git a/src/persistence/projects_directory.py b/src/persistence/projects_directory.py
new file mode 100644 (file)
index 0000000..c48cc60
--- /dev/null
@@ -0,0 +1,53 @@
+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() 
+            
diff --git a/tests/specs/__init__.py b/tests/specs/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/specs/logic/__init__.py b/tests/specs/logic/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/specs/logic/review_visitor_spec.py b/tests/specs/logic/review_visitor_spec.py
new file mode 100644 (file)
index 0000000..14646f0
--- /dev/null
@@ -0,0 +1,78 @@
+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)
diff --git a/tests/specs/model/__init__.py b/tests/specs/model/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/specs/model/action_spec.py b/tests/specs/model/action_spec.py
new file mode 100644 (file)
index 0000000..92b1a95
--- /dev/null
@@ -0,0 +1,63 @@
+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)
diff --git a/tests/specs/model/filtered_list_spec.py b/tests/specs/model/filtered_list_spec.py
new file mode 100644 (file)
index 0000000..94f3e93
--- /dev/null
@@ -0,0 +1,69 @@
+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)
diff --git a/tests/specs/model/model_spec.py b/tests/specs/model/model_spec.py
new file mode 100644 (file)
index 0000000..373175e
--- /dev/null
@@ -0,0 +1,10 @@
+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)
diff --git a/tests/specs/model/observable_spec.py b/tests/specs/model/observable_spec.py
new file mode 100644 (file)
index 0000000..924adb8
--- /dev/null
@@ -0,0 +1,52 @@
+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)
diff --git a/tests/specs/model/project_spec.py b/tests/specs/model/project_spec.py
new file mode 100644 (file)
index 0000000..12f6679
--- /dev/null
@@ -0,0 +1,326 @@
+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
+
+
+           
+
+
+
diff --git a/tests/specs/model/projects_spec.py b/tests/specs/model/projects_spec.py
new file mode 100644 (file)
index 0000000..8c8f414
--- /dev/null
@@ -0,0 +1,53 @@
+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)
diff --git a/tests/specs/persistence/__init__.py b/tests/specs/persistence/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/specs/persistence/action_file_spec.py b/tests/specs/persistence/action_file_spec.py
new file mode 100644 (file)
index 0000000..99ee967
--- /dev/null
@@ -0,0 +1,148 @@
+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
+
diff --git a/tests/specs/persistence/file_based_spec.py b/tests/specs/persistence/file_based_spec.py
new file mode 100644 (file)
index 0000000..c13b774
--- /dev/null
@@ -0,0 +1,43 @@
+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"]
diff --git a/tests/specs/persistence/project_file_spec.py b/tests/specs/persistence/project_file_spec.py
new file mode 100644 (file)
index 0000000..28ab2f2
--- /dev/null
@@ -0,0 +1,223 @@
+# 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
diff --git a/tests/specs/persistence/projects_directory_spec.py b/tests/specs/persistence/projects_directory_spec.py
new file mode 100644 (file)
index 0000000..8f5e20f
--- /dev/null
@@ -0,0 +1,112 @@
+# 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))
+        
diff --git a/tests/specs/ui_spec.py b/tests/specs/ui_spec.py
new file mode 100644 (file)
index 0000000..c316d70
--- /dev/null
@@ -0,0 +1,15 @@
+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()
diff --git a/tests/test_all.py b/tests/test_all.py
new file mode 100644 (file)
index 0000000..21c3aee
--- /dev/null
@@ -0,0 +1,36 @@
+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()])