nutools/lib/pyulib/migrate/tasks1/TODO.py

746 lines
26 KiB
Python
Raw Normal View History

2013-08-27 15:14:44 +04:00
# -*- 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
)