Skip to content

Commit

Permalink
Repeating timestamps, new class OrgTodo, better handling of TODO stat…
Browse files Browse the repository at this point in the history
…es in

OrgNode, split TODO states into OrgNode.todo_states and OrgNode.done_states,
and added method OrgDataStructure.extract_todo_list.

Repeating timestamps can now be stored with the "+4w" string saved as
an attribute.

The new class OrgTodo holds individual todo items, useful for making todo lists
and agendas.

TODO states are now output properly in OrgNode._output().

There are now two lists of todo states: todo_states and done_states. A new method
was added to OrgDataStructure (add_todo_state) that allows for adding to the latter
list.

New method OrgDataStructure.extract_todo_list returns a list of OrgTodo states.
By default it returns states listed in OrgNode.todo_states but this can be modified
by arguments. It will raise an exception if a todo state is passed that is not on
either list since these will not be extracted properly during OrgDataStructure.load_from_file().
  • Loading branch information
Mark Wolf committed Oct 2, 2012
1 parent a119702 commit d0ec4d9
Showing 1 changed file with 171 additions and 40 deletions.
211 changes: 171 additions & 40 deletions PyOrgMode.py
Expand Up @@ -41,12 +41,14 @@ class OrgDate:
ACTIVE = 8
INACTIVE = 16
RANGED = 32
REPEAT = 64

# TODO: Timestamp with repeater interval
DICT_RE = {'start': '[[<]',
'end': '[]>]',
'date': '([0-9]{4})-([0-9]{2})-([0-9]{2})(\s+([\w]+))?',
'time': '([0-9]{2}):([0-9]{2})'}
'time': '([0-9]{2}):([0-9]{2})',
'repeat': '[\+\.]{1,2}\d+[dwmy]'}

def __init__(self,value=None):
"""
Expand All @@ -58,7 +60,7 @@ def __init__(self,value=None):
def parse_datetime(self, s):
"""
Parses an org-mode date time string.
Returns (timed, weekdayed, time_struct).
Returns (timed, weekdayed, time_struct, repeat).
"""
search_re = '(?P<date>{date})(\s+(?P<time>{time}))?'.format(
**self.DICT_RE)
Expand Down Expand Up @@ -120,12 +122,14 @@ def set_value(self,value):
self.format |= self.DATED | self.RANGED
return
# single date with no range
search_re = '{start}(?P<datetime>{date}(\s+{time})?){end}'.format(
**self.DICT_RE)
search_re = '{start}(?P<datetime>{date}(\s+{time})?)(\s+(?P<repeat>{repeat}))?{end}'.format(**self.DICT_RE)
match = re.search(search_re, value)
if match:
timed, weekdayed, self.value = self.parse_datetime(
match.group('datetime'))
if match.group('repeat'):
self.repeat = match.group('repeat')
self.format |= self.REPEAT
self.format |= self.DATED
if timed:
self.format |= self.TIMED
Expand Down Expand Up @@ -169,14 +173,18 @@ def get_value(self):
time.strftime(
'{start}{date}{end}'.format(**fmt_dict),
self.end))
else:
# non-ranged time
else: # non-ranged time
# Repeated
if self.format & self.REPEAT:
fmt_dict['repeat'] = ' ' + self.repeat
else:
fmt_dict['repeat'] = ''
if self.format & self.TIMED:
return time.strftime(
'{start}{date} {time}{end}'.format(**fmt_dict), self.value)
'{start}{date} {time}{repeat}{end}'.format(**fmt_dict), self.value)
else:
return time.strftime(
'{start}{date}{end}'.format(**fmt_dict), self.value)
'{start}{date}{repeat}{end}'.format(**fmt_dict), self.value)

class OrgPlugin:
"""
Expand Down Expand Up @@ -251,6 +259,23 @@ def __str__(self):
""" Used to return a text when called. """
return self.output()

class OrgTodo():
"""Describes an individual TODO item for use in agendas and TODO lists"""
def __init__(self, heading, todo_state,
scheduled=None, deadline=None,
tags=None, priority=None,
path=[0]
):
self.heading = heading
self.todo_state = todo_state
self.scheduled = scheduled
self.deadline = deadline
self.tags = tags
self.priority = priority
def __str__(self):
string = self.todo_state + " " + self.heading
return string

class OrgClock(OrgPlugin):
"""Plugin for Clock elements"""
def __init__(self):
Expand Down Expand Up @@ -416,23 +441,25 @@ def _output(self):
class OrgNode(OrgPlugin):
def __init__(self):
OrgPlugin.__init__(self)
self.regexp = re.compile("^(\*+)\s*(\[.*\])?\s*(.*)$")
self.todo_list = ['TODO', 'DONE']
self.todo_list = ['TODO']
self.done_list = ['DONE']
self.keepindent = False # If the line starts by an indent, it is not a node
def _treat(self,current,line):
# Build regexp
regexp_string = "^(\*+)\s*"
if self.todo_list:
separator = ""
re_todos = "("
for todo_keyword in self.todo_list + self.done_list:
re_todos += separator
separator = "|"
re_todos += todo_keyword
re_todos += ")?\s*"
regexp_string += re_todos
regexp_string += "(\[.*\])?\s*(.*)$"
self.regexp = re.compile(regexp_string)
heading = self.regexp.findall(line)
if heading: # We have a heading
# Build the regexp for finding TODO items
if self.todo_list:
separator = ""
regexp_string = "^(\*+)\s*("
for todo_keyword in self.todo_list:
regexp_string += separator
separator = "|"
regexp_string += todo_keyword
regexp_string += ")\s*(\[.*\])?\s*(.*)$"
self.regexp_todo = re.compile(regexp_string)
todo = self.regexp_todo.findall(line)

if current.parent :
current.parent.append(current)
Expand All @@ -449,14 +476,13 @@ def _treat(self,current,line):
# Creating a new node and assigning parameters
current = OrgNode.Element()
current.level = len(heading[0][0])
current.heading = re.sub(":([\w]+):","",heading[0][2]) # Remove tags
current.priority = heading[0][1]
current.heading = re.sub(":([\w]+):","",heading[0][3]) # Remove tags
current.priority = heading[0][2].strip('[#]')
current.parent = parent

if todo: # This item has a todo associated with it
current.todo = todo[0][1]

# Looking for tags
if heading[0][1]:
current.todo = heading[0][1]

# Looking for tags
heading_without_links = re.sub(" \[(.+)\]","",heading[0][2])
current.tags = re.findall(":([\w]+):",heading_without_links)
else:
Expand Down Expand Up @@ -486,11 +512,14 @@ def _output(self):

if hasattr(self,"level"):
output = output + "*"*self.level


if hasattr(self, "todo"):
output = output + " " + self.todo

if self.parent is not None:
output = output + " "
if self.priority:
output = output + self.priority + " "
output = output + "[#" + self.priority + "] "
output = output + self.heading

for tag in self.tags:
Expand Down Expand Up @@ -550,27 +579,61 @@ def load_plugins(self,*arguments,**keywords):
"""
for plugin in arguments:
self.plugins.append(plugin)
def set_todo_states(self,new_todo_states):
def set_todo_states(self,new_states):
"""
Used to override the default list of todo states for any
OrgNode plugins in this object's plugins list. Expects
a list[] of strings as its argument.
a list[] of strings as its argument. The list can be split
by '|' entries into TODO items and DONE items. Anything after
a second '|' will not be processed and be returned.
Setting to an empty list will disable TODO checking.
"""
new_todo_states = []
new_done_states = []
num_lists = 1
# Process the first part of the list (delimited by '|')
for new_state in new_states:
if new_state == '|':
num_lists += 1
break
new_todo_states.append(new_state)
# Clean up the lists so far
if num_lists > 1:
new_states.remove('|')
for todo_state in new_todo_states:
new_states.remove(todo_state)
# Process the second part of the list (delimited by '|')
for new_state in new_states:
if new_state == '|':
num_lists += 1
break
new_done_states.append(new_state)
# Clean up the second list
if num_lists > 2:
new_states.remove('|')
for todo_state in new_done_states:
new_states.remove(todo_state)
# Write the relevant attributes
for plugin in self.plugins:
if plugin.__class__ == OrgNode:
plugin.todo_list = new_todo_states
def get_todo_states(self):
plugin.done_list = new_done_states
if new_states:
return new_states # Return any leftovers
def get_todo_states(self, list_type="todo"):
"""
Returns a list of lists of todo states, one entry for each instance
of OrgNode in this object's plugins list. An empty list means that
instance of OrgNode has TODO checking disabled.
Returns a list of todo states. An empty list means that
instance of OrgNode has TODO checking disabled. The first argument
determines the list that is pulled ("todo"*, "done" or "all").
"""
all_todo_states = []
all_states = []
for plugin in self.plugins:
if plugin.__class__ == OrgNode:
all_todo_states.append(plugin.todo_list)
return all_todo_states
if plugin.todo_list and (list_type == "todo" or list_type == "all"):
all_states += plugin.todo_list
if plugin.done_list and (list_type == "done" or list_type == "all"):
all_states += plugin.done_list
return list(set(all_states))
def add_todo_state(self, new_state):
"""
Appends a todo state to the list of todo states of any OrgNode
Expand All @@ -580,6 +643,74 @@ def add_todo_state(self, new_state):
for plugin in self.plugins:
if plugin.__class__ == OrgNode:
plugin.todo_list.append(new_state)
def add_done_state(self, new_state):
"""
Appends a todo state to the list of todo states of any OrgNode
plugins in this objects plugins list.
Expects a string as its argument.
"""
for plugin in self.plugins:
if plugin.__class__ == OrgNode:
plugin.done_list.append(new_state)
def remove_todo_state(self, old_state):
"""
Remove a given todo state from both the todo list and the done list.
Returns True if the plugin was actually found.
"""
found = False
for plugin in self.plugins:
if plugin.__class__ == OrgNode:
while old_state in plugin.todo_list:
found = True
plugin.todo_list.remove(old_state)
while old_state in plugin.done_list:
found = True
plugin.done_list.remove(old_state)
return found
def extract_agenda(self, start, duration, todo_list):
"""
Extract an agenda of headings that are marked with TODO states.
First argument is the start of the agenda, second argument is duration,
third argument is the list of TODO states to look for.
"""
pass
def extract_todo_list(self, todo_list=None):
"""
Extract a list of headings with TODO states specified by the first argument.
"""
if todo_list == None: # Set default
# Kludge to get around lack of self in function declarations
todo_list = self.get_todo_states()
else:
# Check to make sure all todo_list items are registered
# with the OrgNode plugin
for possible_state in todo_list:
if possible_state not in self.get_todo_states("all"):
raise ValueError("State " + possible_state + " not registered. See PyOrgMode.OrgDataStructure.add_todo_state.")
results_list = []
# Recursive function that steps through each node in current level,
# looking for TODO items and then calls itself to look for
# TODO items one level down.
def extract_from_level(content):
for node in content:
# Check if it's a TODO item and add to results
try:
current_todo = node.todo
except AttributeError:
pass
else: # Handle it
if current_todo in todo_list:
new_todo = OrgTodo(node.heading, node.todo)
results_list.append(new_todo)
# Now check if it has sub-headings
try:
next_content = node.content
except AttributeError:
pass
else: # Hanble it
extract_from_level(next_content)
extract_from_level(self.root.content)
return results_list
def load_from_file(self,name,form="file"):
"""
Used to load an org-file inside this DataStructure
Expand All @@ -591,7 +722,7 @@ def load_from_file(self,name,form="file"):
elif form == "string":
content = name.split("\n")
else:
raise ValueError
raise ValueError("Form \""+form+"\" not recognized")

for line in content:
for plugin in self.plugins:
Expand Down

0 comments on commit d0ec4d9

Please sign in to comment.