746 lines
26 KiB
Python
746 lines
26 KiB
Python
|
# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
|
||
|
|
||
|
__all__ = ["TODO", "TODOFile", "TiddlyTODOFile", "TODOs"]
|
||
|
|
||
|
try: True, False
|
||
|
except NameError: True, False = 1, 0
|
||
|
|
||
|
import os, re, types
|
||
|
from os import path
|
||
|
|
||
|
from base import scriptdir
|
||
|
from dates import *
|
||
|
from files import TextFile
|
||
|
from config import ShConfigFile
|
||
|
from TiddlyWiki import *
|
||
|
|
||
|
_marker = []
|
||
|
|
||
|
def is_seq(value):
|
||
|
return type(value) is types.ListType or type(value) is types.TupleType
|
||
|
|
||
|
def quote_nl(text):
|
||
|
if text is not None:
|
||
|
text = text.replace("\\", r"\\")
|
||
|
text = text.replace("\n", r"\n")
|
||
|
return text
|
||
|
|
||
|
def unquote_nl(text):
|
||
|
if text is not None:
|
||
|
text = text.replace(r"\n", "\n")
|
||
|
text = text.replace(r"\\", "\\")
|
||
|
return text
|
||
|
|
||
|
class TODO:
|
||
|
def __init__(self, title=None, desc=None, contexts=None, projects=None, tags=None, todonum=None, todoline=None):
|
||
|
self.todonum = todonum
|
||
|
self.title = title
|
||
|
self.desc = desc
|
||
|
if title is not None:
|
||
|
self.text = quote_nl(title + (desc and "\n" + desc or ""))
|
||
|
self.contexts = contexts or ()
|
||
|
self.projects = projects or ()
|
||
|
self.tags = tags or {}
|
||
|
if todoline is not None: self.parse(todoline)
|
||
|
|
||
|
RE_CONTEXT = re.compile(r'@([^ ]+)\s+')
|
||
|
RE_PROJECT = re.compile(r'\+([^ ]+)\s+')
|
||
|
RE_TAG = re.compile(r'([^= ]+)=([^ ]+)\s+')
|
||
|
|
||
|
def parse(self, line):
|
||
|
contexts = []
|
||
|
projects = []
|
||
|
tags = {}
|
||
|
title = ''
|
||
|
desc = None
|
||
|
|
||
|
while True:
|
||
|
mo = self.RE_CONTEXT.match(line)
|
||
|
if mo is not None:
|
||
|
contexts.append(mo.group(1))
|
||
|
line = line[mo.end(0):]
|
||
|
continue
|
||
|
mo = self.RE_PROJECT.match(line)
|
||
|
if mo is not None:
|
||
|
projects.append(mo.group(1))
|
||
|
line = line[mo.end(0):]
|
||
|
continue
|
||
|
mo = self.RE_TAG.match(line)
|
||
|
if mo is not None:
|
||
|
tags[mo.group(1)] = mo.group(2)
|
||
|
line = line[mo.end(0):]
|
||
|
continue
|
||
|
break
|
||
|
|
||
|
text = line
|
||
|
pos = text.find("\\n")
|
||
|
if pos == -1:
|
||
|
title = text
|
||
|
desc = None
|
||
|
else:
|
||
|
title = text[:pos]
|
||
|
desc = text[pos+2:]
|
||
|
|
||
|
self.text = text
|
||
|
self.title = unquote_nl(title)
|
||
|
self.desc = unquote_nl(desc)
|
||
|
self.contexts = contexts
|
||
|
self.projects = projects
|
||
|
self.tags = tags
|
||
|
|
||
|
def to_line(self):
|
||
|
if self.title is None: return None
|
||
|
|
||
|
tags = []
|
||
|
if self.contexts is not None: tags.extend(map(lambda c: "@" + c, self.contexts))
|
||
|
if self.projects is not None: tags.extend(map(lambda p: "+" + p, self.projects))
|
||
|
if self.tags is not None: tags.extend(map(lambda (k, v): "%s=%s" % (k, v), self.tags.items()))
|
||
|
|
||
|
line = ""
|
||
|
if tags: line = line + " ".join(tags) + " "
|
||
|
line = line + quote_nl(self.title)
|
||
|
if self.desc is not None: line = line + "\\n" + quote_nl(self.desc)
|
||
|
|
||
|
return line
|
||
|
|
||
|
def to_string(self):
|
||
|
id = self.tags.get("id", None)
|
||
|
if self.is_done(): string = "TODO[%s], fait le %s:" % (id, self.tags["done"])
|
||
|
else: string = "TODO[%s]:" % id
|
||
|
|
||
|
tags = []
|
||
|
if self.contexts is not None: tags.extend(map(lambda c: "@" + c, self.contexts))
|
||
|
if self.projects is not None: tags.extend(map(lambda p: "+" + p, self.projects))
|
||
|
for tag in self.tags:
|
||
|
if tag != "id" and tag != "done" and not tag.endswith("_purged"):
|
||
|
tags.append("%s=%s" % (tag, self.tags[tag]))
|
||
|
if tags: string = string + " " + " ".join(tags)
|
||
|
string = string + "\n"
|
||
|
|
||
|
string = string + self.title
|
||
|
if self.desc: string = string + "\n\n" + self.desc
|
||
|
|
||
|
return string
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "TODO(%s)" % self.title
|
||
|
|
||
|
# Accès aux tags
|
||
|
def __len__(self):
|
||
|
return len(self.tags)
|
||
|
def __getitem__(self, key, default=_marker):
|
||
|
if default is _marker: return self.tags[key]
|
||
|
else: return self.tags.get(key, default)
|
||
|
get = __getitem__
|
||
|
def __setitem__(self, key, value):
|
||
|
self.tags[key] = value
|
||
|
def __delitem__(self, key):
|
||
|
del self.tags[key]
|
||
|
def __contains__(self, key):
|
||
|
return key in self.tags
|
||
|
def has_key(self, key):
|
||
|
return self.tags.has_key(key)
|
||
|
def keys(self):
|
||
|
return self.tags.keys()
|
||
|
def values(self):
|
||
|
return self.tags.values()
|
||
|
def items(self):
|
||
|
return self.tags.items()
|
||
|
|
||
|
def is_done(self):
|
||
|
return self.has_key("done")
|
||
|
def is_project_purged(self, project):
|
||
|
return self.has_key("%s_purged" % project)
|
||
|
def set_project_purged(self, project, date=None):
|
||
|
if date is None: date = datef()
|
||
|
self.tags["%s_purged" % project] = date
|
||
|
|
||
|
def has_projects(self):
|
||
|
return len(self.projects) != 0
|
||
|
def is_all_purged(self):
|
||
|
for project in self.projects:
|
||
|
if not self.is_project_purged(project): return False
|
||
|
return True
|
||
|
|
||
|
def is_project_released(self, project):
|
||
|
return self.has_key("%s_released" % project)
|
||
|
def set_project_released(self, project, date=None):
|
||
|
if date is None: date = datef()
|
||
|
self.tags["%s_released" % project] = date
|
||
|
|
||
|
|
||
|
# Filtre
|
||
|
def filter(self, filters=None, **kw):
|
||
|
if filters is None: filters = kw
|
||
|
else: filters.update(kw)
|
||
|
for filter, value in filters.items():
|
||
|
if filter == "project":
|
||
|
if is_seq(value):
|
||
|
projects = value
|
||
|
for project in projects:
|
||
|
if project not in self.projects: return False
|
||
|
else:
|
||
|
project = value
|
||
|
if project not in self.projects: return False
|
||
|
elif filter == "context":
|
||
|
if is_seq(value):
|
||
|
contexts = value
|
||
|
for context in contexts:
|
||
|
if context not in self.contexts: return False
|
||
|
else:
|
||
|
context = value
|
||
|
if context not in self.contexts: return False
|
||
|
elif filter == "todonum":
|
||
|
if self.todonum != value: return False
|
||
|
else:
|
||
|
if value is None:
|
||
|
if filter in self.tags: return False
|
||
|
elif value == '*':
|
||
|
if filter not in self.tags: return False
|
||
|
else:
|
||
|
if filter not in self.tags or self.tags[filter] != value: return False
|
||
|
return True
|
||
|
|
||
|
class TODOFile(TextFile):
|
||
|
def __init__(self, file, DONE_file=False, raise_exception=False):
|
||
|
TextFile.__init__(self)
|
||
|
|
||
|
TODO_txt = DONE_file and 'DONE.txt' or 'TODO.txt'
|
||
|
todo_txt = DONE_file and 'done.txt' or 'todo.txt'
|
||
|
if path.isdir(file):
|
||
|
# on a donné le nom d'un répertoire. y chercher un fichier nommé
|
||
|
# TODO.txt ou todo.txt
|
||
|
dir = file
|
||
|
todofile = path.join(dir, TODO_txt)
|
||
|
if not path.exists(todofile):
|
||
|
todofile2 = path.join(dir, todo_txt)
|
||
|
if path.exists(todofile2): todofile = todofile2
|
||
|
else:
|
||
|
# on a donné un nom de fichier
|
||
|
todofile = file
|
||
|
self.load(todofile, raise_exception=raise_exception)
|
||
|
|
||
|
def __update_todonum(self):
|
||
|
todonum = 1
|
||
|
for todo in self.todos:
|
||
|
todo.todonum = todonum
|
||
|
todonum = todonum + 1
|
||
|
|
||
|
def readlines(self, raise_exception=True):
|
||
|
TextFile.readlines(self, raise_exception=raise_exception)
|
||
|
|
||
|
self.todos = []
|
||
|
for line in self.lines:
|
||
|
self.todos.append(TODO(todoline=line))
|
||
|
self.__update_todonum()
|
||
|
|
||
|
def writelines(self):
|
||
|
"""Ecrire les TODOS et les DONEs dans le fichier
|
||
|
"""
|
||
|
lines = []
|
||
|
for todo in self.todos:
|
||
|
lines.append(todo.to_line())
|
||
|
|
||
|
self.lines = lines
|
||
|
TextFile.writelines(self)
|
||
|
save = writelines
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.todos)
|
||
|
def __getitem__(self, index):
|
||
|
return self.todos[index]
|
||
|
|
||
|
def list(self, filters=None, **kw):
|
||
|
if type(filters) is types.IntType: return [self.todos[filters]]
|
||
|
elif isinstance(filters, TODO):
|
||
|
todo = filters
|
||
|
if todo in self.todos: return [todo]
|
||
|
else:
|
||
|
id = todo.get("id", None)
|
||
|
if id is None: return []
|
||
|
filters = {"id": id}
|
||
|
|
||
|
if filters is None: filters = kw
|
||
|
else: filters.update(kw)
|
||
|
return filter(lambda t: t.filter(filters), self.todos)
|
||
|
|
||
|
def get(self, filters=None, **kw):
|
||
|
todos = self.list(filters, **kw)
|
||
|
if len(todos) == 0: return None
|
||
|
elif len(todos) > 1: raise ValueError("Plusieurs tâches correspondent aux critères")
|
||
|
else: return todos[0]
|
||
|
|
||
|
def remove(self, filters=None, **kw):
|
||
|
todos = self.list(filters, **kw)
|
||
|
for todo in todos:
|
||
|
self.todos.remove(todo)
|
||
|
self.__update_todonum()
|
||
|
|
||
|
def put(self, todo):
|
||
|
if not isinstance(todo, TODO): todo = TODO(todoline=todo)
|
||
|
self.remove(todo)
|
||
|
self.todos.append(todo)
|
||
|
self.__update_todonum()
|
||
|
return todo
|
||
|
|
||
|
def get_file(self):
|
||
|
return self.pf
|
||
|
|
||
|
def can_release(self):
|
||
|
return False
|
||
|
|
||
|
def release(self, released, release_date=None, version=None, changes=None):
|
||
|
pass
|
||
|
|
||
|
class TiddlyTODOFile:
|
||
|
RE_DATE = re.compile(r'(\d\d)/(\d\d)/(\d\d\d\d)$')
|
||
|
RE_ID = re.compile(r'(\d\d)/(\d\d)/(\d\d\d\d)-(\d+)$')
|
||
|
|
||
|
def __init__(self, file, project):
|
||
|
if path.isdir(file):
|
||
|
file = path.join(file, "TODO.html")
|
||
|
self.tw = TiddlyWiki(file)
|
||
|
self.project = project
|
||
|
|
||
|
def get_tiddler(self, id):
|
||
|
mo = self.RE_ID.match(id)
|
||
|
if mo is not None: tiddler = u"TODO-%s%s%s-%s" % (mo.group(3), mo.group(2), mo.group(1), mo.group(4))
|
||
|
else: tiddler = ensure_unicode(id)
|
||
|
return tiddler
|
||
|
|
||
|
def get(self, id):
|
||
|
return tw[self.get_tiddler(id)]
|
||
|
|
||
|
def remove(self, todo_or_id):
|
||
|
if isinstance(todo_or_id, TODO):
|
||
|
todo = todo_or_id
|
||
|
id = todo.get("id", None)
|
||
|
else:
|
||
|
id = todo_or_id
|
||
|
if id is None: raise ValueError("Il n'est possible de mettre à jour qu'un TODO avec un id")
|
||
|
|
||
|
self.tw.remove(self.get_tiddler(id))
|
||
|
|
||
|
def put(self, todo):
|
||
|
id = todo.get("id", None)
|
||
|
if id is None: raise ValueError("Il n'est possible de mettre à jour qu'un TODO avec un id")
|
||
|
|
||
|
tiddler = self.get_tiddler(id)
|
||
|
modifier = None
|
||
|
modified = None
|
||
|
|
||
|
twtext = todo.text
|
||
|
twtags = map(lambda c: "@" + c, todo.contexts) + map(lambda p: "+" + p, todo.projects)
|
||
|
if "date" in todo.tags:
|
||
|
mo = self.RE_DATE.match(todo.tags["date"])
|
||
|
if mo is not None:
|
||
|
twtags.append("TODO-%s%s%s" % (mo.group(3), mo.group(2), mo.group(1)))
|
||
|
else:
|
||
|
twtags.append("TODO-%s" % todo.tags["date"])
|
||
|
if todo.is_done():
|
||
|
donetag = todo.is_project_released(self.project) and "RELEASED" or "DONE"
|
||
|
twtags.append(donetag)
|
||
|
twtext = twtext + "\n\nFAIT le %s" % todo.tags["done"]
|
||
|
else:
|
||
|
twtags.append("TODO")
|
||
|
|
||
|
self.tw.put(Tiddler(tiddler, twtext, " ".join(twtags), modifier, modified))
|
||
|
|
||
|
def save(self):
|
||
|
self.tw.save()
|
||
|
|
||
|
def get_file(self):
|
||
|
return self.tw.twpf
|
||
|
|
||
|
def can_release(self):
|
||
|
return True
|
||
|
|
||
|
def release(self, released, release_date=None, version=None, changes=None):
|
||
|
release_name = datef("Release-%Y%m%d", release_date)
|
||
|
tiddler = release_name
|
||
|
count = 0
|
||
|
for tiddler in self.tw:
|
||
|
t = self.tw[tiddler]
|
||
|
if t.tags_contains(release_name):
|
||
|
count = count + 1
|
||
|
if count == 0: tiddler = release_name
|
||
|
else: tiddler = "%s-%i" % (release_name, count)
|
||
|
|
||
|
twtext = u"release du %s\nversion: %s\n" % (datef(FR_DATEF, release_date), version)
|
||
|
if changes:
|
||
|
twtext = twtext + "\n" + ensure_unicode(changes)
|
||
|
if released:
|
||
|
twtext = twtext + "\n"
|
||
|
for done in released:
|
||
|
twtext = twtext + "* <<todoLink %s>>\n" % self.get_tiddler(done.get("id", None))
|
||
|
twtags = [release_name, "release"]
|
||
|
|
||
|
self.tw.put(Tiddler(tiddler, twtext, " ".join(twtags)))
|
||
|
|
||
|
class TODOs:
|
||
|
DEFAULTS = {"scriptdir": scriptdir}
|
||
|
|
||
|
def __init__(self, todorcfile=None):
|
||
|
if todorcfile is None:
|
||
|
todorcfile = path.join(os.environ["HOME"], ".utools/todorc")
|
||
|
if path.exists(todorcfile):
|
||
|
todorc = ShConfigFile(todorcfile, defaults=self.DEFAULTS)
|
||
|
else:
|
||
|
todorc = self.DEFAULTS
|
||
|
tododir = todorc.get("tododir", path.split(todorcfile)[0])
|
||
|
todofile = todorc.get("todofile", path.join(tododir, "TODO.txt"))
|
||
|
donefile = todorc.get("donefile", path.join(tododir, "DONE.txt"))
|
||
|
projdirs = todorc.get("projdirs", "")
|
||
|
|
||
|
self.tododir = tododir
|
||
|
self.todofile = todofile
|
||
|
self.donefile = donefile
|
||
|
self.projdirs = filter(None, projdirs.split(":"))
|
||
|
|
||
|
self.todos = TODOFile(self.todofile)
|
||
|
self.dones = TODOFile(self.donefile)
|
||
|
|
||
|
def get_projdir(self, project):
|
||
|
for projdir in self.projdirs:
|
||
|
dir = path.join(projdir, project)
|
||
|
if path.isdir(dir):
|
||
|
return dir
|
||
|
return None
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.todos)
|
||
|
def __getitem__(self, index):
|
||
|
return self.todos[index]
|
||
|
|
||
|
def __str__(self):
|
||
|
lines = []
|
||
|
for todo in self.todos:
|
||
|
lines.append("%i: %s" % (todo.todonum, todo.to_string()))
|
||
|
return "\n".join(lines)
|
||
|
|
||
|
def p(self):
|
||
|
print self
|
||
|
|
||
|
## Interface utilisateur
|
||
|
def list(self, filters=None, **kw):
|
||
|
return self.todos.list(filters, **kw)
|
||
|
|
||
|
def get(self, filters=None, **kw):
|
||
|
todos = self.list(filters, **kw)
|
||
|
if len(todos) == 0: return None
|
||
|
elif len(todos) > 1: raise ValueError("Plusieurs tâches correspondent aux critères")
|
||
|
else: return todos[0]
|
||
|
|
||
|
def __put_in_projects(self, todo, DONE_file=False):
|
||
|
for project in todo.projects:
|
||
|
projdir = self.get_projdir(project)
|
||
|
if projdir is not None:
|
||
|
twhtml = path.join(projdir, "TODO.html")
|
||
|
if path.exists(twhtml):
|
||
|
todofile = TiddlyTODOFile(twhtml, project)
|
||
|
else:
|
||
|
if DONE_file:
|
||
|
todofile = TODOFile(projdir, DONE_file=False)
|
||
|
todofile.remove(todo)
|
||
|
todofile.save()
|
||
|
todofile = TODOFile(projdir, DONE_file=DONE_file)
|
||
|
todofile.put(todo)
|
||
|
todofile.save()
|
||
|
|
||
|
def put(self, todo):
|
||
|
todo = self.todos.put(todo)
|
||
|
self.__put_in_projects(todo)
|
||
|
self.todos.save()
|
||
|
|
||
|
def add(self, title=None, desc=None, contexts=None, projects=None, tags=None, set_done=False, todoline=None):
|
||
|
tags = tags or {}
|
||
|
# Calculer la date
|
||
|
date = datef()
|
||
|
if not tags.has_key("date"): tags["date"] = date
|
||
|
# Faut-il finir la tâche tout de suite?
|
||
|
if set_done and not tags.has_key("done"):
|
||
|
tags["done"] = date
|
||
|
# Calculer l'id
|
||
|
if not tags.has_key("id"):
|
||
|
count = 1
|
||
|
for todo in self.todos:
|
||
|
if todo.has_key("id") and todo["id"].startswith(date + "-"): count = count + 1
|
||
|
elif todo.has_key("date") and todo["date"] == date: count = count + 1
|
||
|
for todo in self.dones:
|
||
|
if todo.has_key("id") and todo["id"].startswith(date + "-"): count = count + 1
|
||
|
elif todo.has_key("date") and todo["date"] == date: count = count + 1
|
||
|
tags["id"] = "%s-%i" % (date, count)
|
||
|
|
||
|
if todoline is None:
|
||
|
todo = TODO(title, desc, contexts, projects, tags)
|
||
|
else:
|
||
|
todo = TODO(todoline=todoline)
|
||
|
todo.tags.update(tags)
|
||
|
|
||
|
self.put(todo)
|
||
|
|
||
|
def do(self, filters=None, **kw):
|
||
|
todos = self.list(filters, **kw)
|
||
|
date = datef()
|
||
|
modified = False
|
||
|
for todo in todos:
|
||
|
if not todo.is_done():
|
||
|
todo["done"] = date
|
||
|
if todo.has_key("pri"): del todo["pri"]
|
||
|
modified = True
|
||
|
self.__put_in_projects(todo)
|
||
|
if modified: self.todos.save()
|
||
|
|
||
|
def __remove_from_projects(self, todo):
|
||
|
for project in todo.projects:
|
||
|
projdir = self.get_projdir(project)
|
||
|
if projdir is not None:
|
||
|
twhtml = path.join(projdir, "TODO.html")
|
||
|
if path.exists(twhtml): todofile = TiddlyTODOFile(twhtml, project)
|
||
|
else: todofile = TODOFile(projdir)
|
||
|
todofile.remove(todo)
|
||
|
todofile.save()
|
||
|
|
||
|
def remove(self, filters=None, **kw):
|
||
|
todos = self.list(filters, **kw)
|
||
|
modified = False
|
||
|
for todo in todos:
|
||
|
self.todos.remove(todo)
|
||
|
modified = True
|
||
|
self.__remove_from_projects(todo)
|
||
|
if modified: self.todos.save()
|
||
|
|
||
|
def purge(self, project=None):
|
||
|
modified = False
|
||
|
if project is None:
|
||
|
# purger les tâches qui ne sont assignées à aucun projet
|
||
|
todos = self.todos.list(done='*')
|
||
|
dones = []
|
||
|
for todo in todos:
|
||
|
if not todo.has_projects():
|
||
|
dones.append(todo)
|
||
|
for done in dones:
|
||
|
self.dones.put(done)
|
||
|
self.todos.remove(done)
|
||
|
modified = True
|
||
|
self.__put_in_projects(done, DONE_file=True)
|
||
|
else:
|
||
|
# D'abord calculer les tâches qui sont elligibles à la purge
|
||
|
todos = self.todos.list(project=project, done='*')
|
||
|
# Puis les marquer comme purgées
|
||
|
dones = []
|
||
|
purgeable = []
|
||
|
for todo in todos:
|
||
|
if not todo.is_project_purged(project):
|
||
|
todo.set_project_purged(project)
|
||
|
modified = True
|
||
|
if todo.is_all_purged(): purgeable.append(todo)
|
||
|
else: dones.append(todo)
|
||
|
for done in dones:
|
||
|
self.__put_in_projects(done)
|
||
|
for done in purgeable:
|
||
|
self.dones.put(done)
|
||
|
self.todos.remove(done)
|
||
|
modified = True
|
||
|
self.__put_in_projects(done, DONE_file=True)
|
||
|
|
||
|
if modified:
|
||
|
self.todos.save()
|
||
|
self.dones.save()
|
||
|
|
||
|
def list_releaseable(self, project):
|
||
|
"""Lister les tâches qui sont concernées par self.release()
|
||
|
"""
|
||
|
releaseable = []
|
||
|
# chercher dans self.todos
|
||
|
purged = self.todos.list(project=project, done='*')
|
||
|
purged = filter(lambda t: t.is_project_purged(project), purged)
|
||
|
for todo in purged:
|
||
|
if not todo.is_project_released(project):
|
||
|
releaseable.append(todo)
|
||
|
# puis dans self.dones
|
||
|
purged = self.dones.list(project=project, done='*')
|
||
|
purged = filter(lambda t: t.is_project_purged(project), purged)
|
||
|
for todo in purged:
|
||
|
if not todo.is_project_released(project):
|
||
|
releaseable.append(todo)
|
||
|
return releaseable
|
||
|
|
||
|
def mark_released(self, project):
|
||
|
"""Marquer les tâches du projet comme released
|
||
|
Retourner le TODOFile ou TiddlyTODOFile qui correspond au projet
|
||
|
"""
|
||
|
#released = []
|
||
|
# D'abord calculer les tâches qui sont elligibles à la release, puis les marquer comme released
|
||
|
# d'abord pour self.todos
|
||
|
purged = self.todos.list(project=project, done='*')
|
||
|
purged = filter(lambda t: t.is_project_purged(project), purged)
|
||
|
modified = False
|
||
|
for todo in purged:
|
||
|
if not todo.is_project_released(project):
|
||
|
todo.set_project_released(project)
|
||
|
modified = True
|
||
|
#released.append(todo)
|
||
|
# Mettre à jour les projets
|
||
|
self.__put_in_projects(todo)
|
||
|
if modified: self.todos.save()
|
||
|
# puis pour self.dones
|
||
|
purged = self.dones.list(project=project, done='*')
|
||
|
purged = filter(lambda t: t.is_project_purged(project), purged)
|
||
|
modified = False
|
||
|
for todo in purged:
|
||
|
if not todo.is_project_released(project):
|
||
|
todo.set_project_released(project)
|
||
|
modified = True
|
||
|
#released.append(todo)
|
||
|
# Mettre à jour les projets
|
||
|
self.__put_in_projects(todo, DONE_file=True)
|
||
|
if modified: self.dones.save()
|
||
|
|
||
|
def get_todofile(self, project):
|
||
|
"""Obtenir le TODOFile ou le TiddlyTODOFile correspondant au projet, ou
|
||
|
None si le projet est invalide
|
||
|
"""
|
||
|
projdir = self.get_projdir(project)
|
||
|
if projdir is not None:
|
||
|
twhtml = path.join(projdir, "TODO.html")
|
||
|
if path.exists(twhtml): todofile = TiddlyTODOFile(twhtml, project)
|
||
|
else: todofile = TODOFile(projdir)
|
||
|
return todofile
|
||
|
return None
|
||
|
|
||
|
class Report:
|
||
|
# True si une tâche avec priorité a été rencontrée
|
||
|
pri = False
|
||
|
# Nom
|
||
|
name = None
|
||
|
# True si une tâche valide a été rencontrée
|
||
|
valid = False
|
||
|
# statistiques pour les tâches valides
|
||
|
valid_open = 0
|
||
|
valid_closed = 0
|
||
|
valid_total = 0
|
||
|
valid_open_perc = 0
|
||
|
valid_closed_perc = 0
|
||
|
# statistiques pour les tâches purgées
|
||
|
purged_open = 0
|
||
|
purged_closed = 0
|
||
|
purged_total = 0
|
||
|
purged_open_perc = 0
|
||
|
purged_closed_perc = 0
|
||
|
# statistiques pour toutes les tâches
|
||
|
open = 0
|
||
|
closed = 0
|
||
|
total = 0
|
||
|
open_perc = 0
|
||
|
closed_perc = 0
|
||
|
|
||
|
def __init__(self, name):
|
||
|
self.name = name
|
||
|
|
||
|
def update_perc(self):
|
||
|
if self.valid_total > 0:
|
||
|
self.valid_open_perc = self.valid_open * 100 / self.valid_total
|
||
|
self.valid_closed_perc = self.valid_closed * 100 / self.valid_total
|
||
|
if self.purged_total > 0:
|
||
|
self.purged_open_perc = self.purged_open * 100 / self.purged_total
|
||
|
self.purged_closed_perc = self.purged_closed * 100 / self.purged_total
|
||
|
if self.total > 0:
|
||
|
self.open_perc = self.open * 100 / self.total
|
||
|
self.closed_perc = self.closed * 100 / self.total
|
||
|
def update_open(self, valid=True):
|
||
|
if valid:
|
||
|
self.valid_open = self.valid_open + 1
|
||
|
self.valid_total = self.valid_total + 1
|
||
|
else:
|
||
|
self.purged_open = self.purged_open + 1
|
||
|
self.purged_total = self.purged_total + 1
|
||
|
self.open = self.open + 1
|
||
|
self.total = self.total + 1
|
||
|
self.update_perc()
|
||
|
def update_closed(self, valid=True):
|
||
|
if valid:
|
||
|
self.valid_closed = self.valid_closed + 1
|
||
|
self.valid_total = self.valid_total + 1
|
||
|
else:
|
||
|
self.purged_closed = self.purged_closed + 1
|
||
|
self.purged_total = self.purged_total + 1
|
||
|
self.closed = self.closed + 1
|
||
|
self.total = self.total + 1
|
||
|
self.update_perc()
|
||
|
def update(self, todo, valid=False):
|
||
|
if todo.is_done(): self.update_closed(valid)
|
||
|
else: self.update_open(valid)
|
||
|
self.valid = self.valid or valid
|
||
|
|
||
|
def valid_closed_bar(self, max=20):
|
||
|
n = self.valid_closed_perc * max / 100
|
||
|
return "=" * n + (max - n) * " "
|
||
|
|
||
|
def purged_closed_bar(self, max=20):
|
||
|
n = self.purged_closed_perc * max / 100
|
||
|
return "=" * n + (max - n) * " "
|
||
|
|
||
|
def closed_bar(self, max=20):
|
||
|
n = self.closed_perc * max / 100
|
||
|
return "=" * n + (max - n) * " "
|
||
|
|
||
|
def print_reports(self, title, reports):
|
||
|
if not reports: return
|
||
|
|
||
|
print title
|
||
|
print "-" * len(title)
|
||
|
print
|
||
|
|
||
|
def cmp_report(r0, r1):
|
||
|
r = cmp(r0.pri, r1.pri)
|
||
|
if r != 0: return r
|
||
|
r = -cmp(r0.valid_closed_perc, r1.valid_closed_perc)
|
||
|
if r != 0: return r
|
||
|
return cmp(r0.name, r1.name)
|
||
|
reports.sort(cmp_report)
|
||
|
|
||
|
maxnamelen = reduce(max, map(lambda r: len(r.name), reports))
|
||
|
for report in reports:
|
||
|
print "%s %3i%% [%s] %*s %2i / %i tâche(s)" % (
|
||
|
report.pri and '*' or ' ',
|
||
|
report.valid_closed_perc,
|
||
|
report.valid_closed_bar(),
|
||
|
maxnamelen, report.name,
|
||
|
report.valid_closed,
|
||
|
report.valid_total
|
||
|
)
|
||
|
print
|
||
|
|
||
|
def do_report(self):
|
||
|
"""Afficher un rapport sur les tâches
|
||
|
"""
|
||
|
projects = {}
|
||
|
contexts = {}
|
||
|
def update_stats(todo, valid, projects=projects, contexts=contexts):
|
||
|
for project in todo.projects:
|
||
|
report = projects.setdefault(project, self.Report("+%s" % project))
|
||
|
report.update(todo, valid)
|
||
|
for context in todo.contexts:
|
||
|
report = contexts.setdefault(context, self.Report("@%s" % context))
|
||
|
report.update(todo, valid)
|
||
|
for todo in self.todos: update_stats(todo, True)
|
||
|
for done in self.dones: update_stats(done, False)
|
||
|
|
||
|
self.print_reports("Projets avec des tâches", filter(lambda p: p.valid, projects.values()))
|
||
|
self.print_reports("Contextes avec des tâches", filter(lambda p: p.valid, contexts.values()))
|
||
|
|
||
|
purged = filter(lambda p: not p.valid, projects.values())
|
||
|
if purged:
|
||
|
title = "Projets terminés (pas de tâches)"
|
||
|
print title
|
||
|
print "-" * len(title)
|
||
|
print
|
||
|
|
||
|
maxnamelen = reduce(max, map(lambda r: len(r.name), purged))
|
||
|
for report in purged:
|
||
|
print " %*s: %2i tâche(s)" % (
|
||
|
maxnamelen, report.name,
|
||
|
report.purged_total
|
||
|
)
|