def cmd_repeat(tl, args): """marks the todo item as done and reenters it after the specified time :description: This command is for frequently occurring todo items, like e.g. a bi-weekly status report. Required fields of :param:`args`: * item: the index number of the item from which something should be detached * date: the relative or absolute date when the item is due again """ with ColorRenderer() as cr: item = tl.get_item_by_index(args.item) # create a copy new_item = tl.add_item(item.text) # we have to create a new ID for the copied item new_item.remove_prop(conf.ID) tl.replace_or_add_prop(new_item, conf.ID, tl.create_tid(new_item)) # set the due date of the new item to the specified date tl.replace_or_add_prop(new_item, conf.DUE, args.date, to_date(args.date)) # set old item to done item.set_to_done() suppress_if_quiet( u"Marked todo item as 'done' and reinserted:\n {item}".format( item=cr.render(new_item)), args)
def cmd_tasked(tl, args): """shows all open todo items that I am tasked with Required fields of :param:`args`: * initiator: for filtering the name used for denoting the initiator * all: if given, also the done todos are shown """ with ColorRenderer() as cr: from_list = collections.defaultdict(list) for item in tl.list_items(): if not args.all and (item.done or item.is_report): continue for initiator in item.delegated_from: from_list[initiator.lower()].append(item) if args.initiator: ini_list = [args.initiator.lower()] else: ini_list = sorted(from_list) nr = 0 for initiator in ini_list: print("Tasks from {delegate}".format( delegate=cr.wrap_delegate(initiator, reset=True))) for item in sorted(from_list[initiator], cmp=tl.default_sort): print(" ", cr.render(item)) nr += 1 suppress_if_quiet(u"{nr} todo items displayed.".format(nr=nr), args)
def cmd_edit(tl, args): """allows editing a given todo item :description: This action will open an editor. If you're done editing, save the file and close the editor or cancel editing by pressing ``Ctrl+C``. * item: the index number of the item to edit """ with ColorRenderer() as cr: if not args.item: open_editor(conf.todo_file) quit(0) item = tl.get_item_by_index(args.item) if not item: print("Could not find item '{item_id}'".format(item_id=args.item)) return print(" ", cr.render(item)) try: output = get_editor_input(item.text) # remove new lines edited_item = TodoItem( output.replace("\r\n", " ").replace("\n", " ").strip()) tl.replace_item(item, edited_item) suppress_if_quiet(u" {item}".format(item=cr.render(edited_item)), args) edited_item.check() except KeyboardInterrupt: # editing has been aborted pass
def cmd_delegated(tl, args): """shows all todo items that have been delegated and wait for input Required fields of :param:`args`: * delegate: for filtering the name used for denoting a delegate * all: if given, also the done todos are shown """ with ColorRenderer() as cr: to_list = collections.defaultdict(list) for item in tl.list_items(): if not args.all and (item.done or item.is_report): continue for delegate in item.delegated_to: to_list[delegate.lower()].append(item) if args.delegate: del_list = [args.delegate.lower()] else: del_list = sorted(to_list) nr = 0 for delegate in del_list: print("Delegated to {delegate}".format( delegate=cr.wrap_delegate(delegate, reset=True))) for item in sorted(to_list[delegate], cmp=tl.default_sort): nr += 1 print(" ", cr.render(item)) suppress_if_quiet(u"{nr} todo items displayed.".format(nr=nr), args)
def cmd_done(tl, args): """sets the status of one or more todo items to 'done' Required fields of :param:`args`: * items: the index number of the items to set to 'done' """ with ColorRenderer() as cr: now = datetime.datetime.now() suppress_if_quiet(u"Marked following todo items as 'done':", args) for item in tl.get_items_by_index_list(args.items): tl.set_to_done(item) # if started property is set, remove it and update duration property if conf.STARTED in item.properties: start_time = item.properties[conf.STARTED] time_delta = now - start_time duration = 0 try: # try to parse existing duration property duration = int(item.properties.get(conf.DURATION, 0)) except: pass # add delta time in minutes duration += int(time_delta.total_seconds() / 60) # remove started property tl.replace_or_add_prop(item, conf.STARTED, None) # update duration property tl.replace_or_add_prop(item, conf.DURATION, duration) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args)
def cmd_unblock(tl, args): """unsetting the first item as a pre-requisite to the second item :description: This allows to remove dependencies between todo items. One item can be removed as a pre-requisite from another todo item. This command is only usable with id support activated. Required fields of :param:`args`: * item: the id of the todo item which is a pre-requisite * blocked: the id of the todo item which is blocked """ with ColorRenderer() as cr: if not conf.id_support: print(u"ID support is deactivated. You cannot use this feature.") return item = tl.get_item_by_index(args.item) blocked = tl.get_item_by_index(args.blocked) if item.tid not in blocked.properties.get(conf.BLOCKEDBY, []): print( u"Todo item '{item_id}' is not a pre-requisite of '{blocked_id}'." .format(item_id=item, blocked_id=blocked)) return tl.remove_prop(blocked, conf.BLOCKEDBY, item.tid) tl.clean_dependencies() tl.reindex() suppress_if_quiet(u" {item}".format(item=cr.render(blocked)), args)
def cmd_search(tl, args): """lists all current and archived todo items that match the search string Required fields of :param:`args`: * search_string: a search string * regex: if given, the search string is interpreted as a regular expression * ci: if given, the search string is interpreted as case insensitive """ with ColorRenderer() as cr: # case insensitivity if args.ci: flags = re.UNICODE | re.IGNORECASE else: flags = re.UNICODE # no search string given if not args.search_string: args.search_string = "." args.regex = True # given as regular expression if args.regex: re_search = re.compile(args.search_string, flags) else: re_search = re.compile(re.escape(args.search_string), flags) # store for all matching items all_matches = [] # first, look at current todo list for item in tl.list_items(): if re_search.search(item.text): all_matches.append((conf.todo_file, item)) # get file list of all archive files by replacing all %x-variables with '*' and # let glob do the hard work file_pattern = re_replace_archive_vars.sub( "*", conf.archive_filename_scheme) root_dir = os.path.dirname(conf.todo_file) file_list = glob.glob(os.path.join(root_dir, file_pattern)) # add the file for items without done timestamp unsorted_file = os.path.join(root_dir, conf.archive_unsorted_filename) if os.path.exists(unsorted_file): file_list.append(unsorted_file) for arch_file in file_list: # create a new todo list for each archive file with TodoList(arch_file) as atl: for item in atl.todolist: if re_search.search(item.text): item.replace_or_add_prop(conf.ID, "(A)") all_matches.append((arch_file, item)) # sort by filename all_matches.sort(key=lambda x: x[0]) # group by filename for filename, items in groupby(all_matches, lambda x: x[0]): print(u"File '{fn}':".format(fn=filename)) for item in items: print(" ", cr.render(item[1])) suppress_if_quiet( u"{nr} matching todo items found".format(nr=len(all_matches)), args)
def cmd_start(tl, args): """sets the 'started' property of an item or lists all started items. :description: If a todo item is picked to be worked on, this command allows setting the started time. Thus, the time it took to work on that item can be derived from 'started' and 'done' time. Required fields of :param:`args`: * item: the index number or id of the todo item which is started """ with ColorRenderer() as cr: if not args.item: for item in tl.list_items(lambda x: True if conf.STARTED in x.properties and not ( x.done or x.is_report) else False): print(u" ", cr.render(item)) else: item = tl.get_item_by_index(args.item) if not item: print(u"No item found with number or ID '{item_id}'".format( item_id=args.item)) return if item.done: print(u"Todo item has already been set to 'done':") print(u" ", cr.render(item)) return if conf.STARTED in item.properties: print(u"Todo item has already been started on {date}".format( date=from_date(item.properties[conf.STARTED]))) print(u" ", cr.render(item)) return now = datetime.datetime.now() tl.replace_or_add_prop(item, conf.STARTED, from_date(now), now)
def cmd_mark(tl, args): """lists all items with markers (e.g. '(!)') :description: Markers can be used to denote a todo item classification, e.g. an open question or an information ('(i)'). If no marker parameter is given, all found markers are listed. Required fields of :param:`args`: * marker: a single character that denotes the type of the marker to list. * all: if given, also the done todo and report items are shown """ with ColorRenderer() as cr: marker_dict = collections.defaultdict(list) for item in tl.list_items(lambda x: True if args.all or not ( x.done or x.is_report) else False): for marker in item.markers: marker_dict[marker].append(item) if args.marker: #show project if the given name (partially) matches the project identifier args_list = [ name for name in sorted(marker_dict) if args.marker == name ] else: # show all sorted projects args_list = sorted(marker_dict) nr = 0 for marker in args_list: print( cr.wrap_marker(u"({marker})".format(marker=marker), reset=True)) for item in sorted(marker_dict[marker], cmp=tl.default_sort): print(u" ", cr.render(item)) nr += 1 suppress_if_quiet(u"{nr} todo items displayed.".format(nr=nr), args)
def cmd_remove(tl, args): """removes one or more items from the todo list Required fields of :param:`args`: * items: the index number of the items to remove * force: if given, confirmation is not requested """ with ColorRenderer() as cr: item_list = tl.get_items_by_index_list(args.items) if not item_list: msg = "Could not find item(s) {item_ids}".format( item_ids=", ".join(args.items)) print(msg) logger.info(msg) return if not args.force: print("Do you really want to remove the following item(s):") for item in item_list: print(" ", cr.render(item)) if confirm_action("Please confirm (y/N): "): for item in item_list: tl.remove_item(item) else: print("Removing aborted") return else: for item in item_list: tl.remove_item(item) msg = u"{nr} todo items ({item_ids}) have been removed.".format( nr=len(item_list), item_ids=",".join( [cr.wrap_id(item.tid, reset=True) for item in item_list])) suppress_if_quiet(msg, args) logger.info(msg)
def cmd_prio(tl, args): """assigns given items a priority (absolute like 'A' or relative like '-') Required fields of :param:`args`: * items: the index numbers of the items to (re)prioritize * priority: the new priority ('A'..'Z' or '+'/'-') or 'x' (for removing) """ with ColorRenderer() as cr: prio_items = tl.get_items_by_index_list(args.items) if not prio_items: print(u"Could not find items {item_ids}".format( item_ids=", ".join(args.items))) return new_prio = args.priority if not re_prio.match(new_prio): print( u"Priority '{prio}' can't be recognized (must be one of A to Z or +/-)" .format(prio=new_prio)) return for item in prio_items: old_prio = item.priority if new_prio == "x": # remove priority suppress_if_quiet(u" Removing priority:", args) tl.set_priority(item, None) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args) elif new_prio == "-": if old_prio in ("Z", None): print(u" Can't lower priority of following item:") print(u" ", cr.render(item)) else: temp_prio = chr(ord(old_prio) + 1) suppress_if_quiet( u" Lower priority from {old_prio} to {new_prio}:". format(old_prio=old_prio, new_prio=temp_prio), args) tl.set_priority(item, temp_prio) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args) elif new_prio == "+": if old_prio in ("A", None): print(u" Can't raise priority of following item:") print(u" ", cr.render(item)) else: temp_prio = chr(ord(old_prio) - 1) suppress_if_quiet( u" Raise priority from {old_prio} to {new_prio}:". format(old_prio=old_prio, new_prio=temp_prio), args) tl.set_priority(item, temp_prio) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args) else: suppress_if_quiet( u" Setting priority from {old_prio} to {new_prio}:". format(old_prio=old_prio, new_prio=new_prio), args) tl.set_priority(item, new_prio) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args)
def cmd_overdue(tl, args): """shows all todo items that are overdue Nor :param:`args` arguments are required. """ with ColorRenderer() as cr: print("Overdue todo items:") nr = 0 for item in tl.list_items(lambda x: not x.done and x.is_overdue()): print(" ", cr.render(item)) nr += 1 suppress_if_quiet(u"{nr} todo items displayed.".format(nr=nr), args)
def cmd_reopen(tl, args): """reopens one or more items marked as 'done' Required fields of :param:`args`: * items: the index numbers of the items to reopen """ with ColorRenderer() as cr: suppress_if_quiet(u"Set the following todo items to open again:", args) for item in tl.get_items_by_index_list(args.items): tl.reopen(item) tl.reindex() suppress_if_quiet(u" {item}".format(item=cr.render(item)), args)
def cmd_call(tl, args): """opens either an URL, a file or mail program depending on information that is attached to the todo item Required fields of :param:`args`: * item: the index number of the item that has either an URL or file attached """ with ColorRenderer() as cr: item = tl.get_item_by_index(args.item) if not item: print(u"Could not find item '{item_id}'".format(item_id=args.item)) return nr = 0 actions = {} for toopen in item.urls: print(u" [{nr: 3d}] Open web site {url}".format(nr=nr, url=toopen)) actions[nr] = (webbrowser.open_new_tab, toopen) nr += 1 for file_name in item.properties.get(conf.FILE, []): if not os.path.exists(file_name): print(u" [xxx] File {fn} does not exist".format(fn=file_name)) else: print( u" [{nr: 3d}] Open file {fn} with default editor".format( nr=nr, fn=file_name)) actions[nr] = (os.startfile, file_name) nr += 1 for email in item.properties.get(conf.MAILTO, []): print( u" [{nr: 3d}] Write a mail to {email} with default mail program" .format(nr=nr, email=email)) actions[nr] = (os.startfile, "mailto:" + email) nr += 1 # simple case: only one action available if len(actions) == 1: actions[0][0](actions[0][1]) elif len(actions) > 1: choice = raw_input( u"Please enter your choice (0-{max:d}): ".format( max=len(actions) - 1)).strip() try: choice = int(choice) except: print(u"Not a valid option. Closing...") quit(-1) if int(choice) in actions: actions[choice][0](actions[choice][1]) else: # nothing available print(u"No files / urls / email addresses found in task:") print(u" ", cr.render(item))
def cmd_check(tl, args): #@UnusedVariable """checks the todo list for syntactical validity Required fields of :param:`args`: """ with ColorRenderer() as cr: nr = 0 for item, warnings in tl.check_items(): nr += 1 print(u" ", cr.render(item)) for warning in warnings: print(u" ", warning) print(u"{nr} warning(s) have been found".format(nr=(nr or "No")))
def cmd_note(tl, args): """adding or editing a note to a todo item :description: Opens a text file that contains further notes for a specific item. Required fields of :param:`args`: * item: the id of the todo item that should be annotated """ with ColorRenderer() as cr: if not conf.id_support: print(u"ID support is deactivated. You cannot use this feature.") return item = tl.get_item_by_index(args.item)
def cmd_delay(tl, args): """delays the due date of one or more todo items Required fields of :param:`args`: * item: the index number of the item to delay * date: either a date or a string like 'tomorrow', default '1d' (delays for 1 day) * force: if given, confirmation is not requested """ with ColorRenderer() as cr: item = tl.get_item_by_index(args.item) if not item: print(u"Could not find item '{item_id}'".format(item_id=args.item)) return if item.due_date: new_date = to_date(args.date, item.due_date) if isinstance(new_date, basestring): # remove first character, as it is "?" with a non-parsable date print(u"The given relative date could not be parsed: {date}". format(date=new_date[1:])) else: # ask for confirmation if not args.force: print(" ", cr.render(item)) if not confirm_action( u"Delaying the preceding item's date from {from_date} to {to_date} (y/N)?" .format(from_date=from_date(item.due_date), to_date=from_date(new_date))): return # do the actual replacement tl.replace_or_add_prop(item, conf.DUE, from_date(new_date), new_date) else: new_date = to_date(args.date) if not args.force: print(u" ", cr.render(item)) if not confirm_action( u"The preceding item has no due date set, set to {date} (y/N)?" .format(date=from_date(new_date))): return tl.replace_or_add_prop(item, conf.DUE, from_date(new_date), new_date) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args)
def cmd_stats(tl, args): #@UnusedVariable """displays some simple statistics about your todo list Required fields of :param:`args`: """ # write # open / # done / # prioritized / # overdue items counter = collections.defaultdict(int) delegates = set() with ColorRenderer() as cr: for item in tl.list_items(): counter["total"] += 1 if item.done: counter["done"] += 1 else: counter["open"] += 1 if item.priority: counter["prioritized"] += 1 if item.is_overdue() and not item.done: counter["overdue"] += 1 if item.is_still_open_today() and not item.done: counter["today"] += 1 if item.is_report: counter["report"] += 1 delegates.update(item.delegated_to) delegates.update(item.delegated_from) print(u"Total number of items: {stat}".format(stat=counter["total"])) print(u"Open items : {stat}".format(stat=counter["open"])) print( cr.wrap_prioritized(u"Prioritized items : {stat}".format( stat=counter["prioritized"]))) print( cr.wrap_overdue(u"Overdue items : {stat}".format( stat=counter["overdue"]))) print( cr.wrap_today(u"Items due today : {stat}".format( stat=counter["today"]))) print( cr.wrap_done( u"Done items : {stat}".format(stat=counter["done"]))) print( cr.wrap_report(u"Report items : {stat}".format( stat=counter["report"])))
def cmd_agenda(tl, args): """displays an agenda for a given date Required fields of :param:`args`: * date: either a date or a string like 'tomorrow' or '*', default 'today' """ with ColorRenderer() as cr: agenda_items = [] # if not set, get agenda for today list_all = False if not args.date: args.date = datetime.datetime.now() elif args.date == "*": list_all = True else: args.date = to_date(args.date) if isinstance(args.date, basestring): print(u"Could not parse date argument '{date_str}'".format( date_str=args.date)) quit(-1) for item in tl.list_items(lambda x: True if x.due_date else False): if is_same_day(args.date, item.due_date) or list_all: agenda_items.append(item) # default date used when no done date is specified na_date = datetime.datetime(1970, 1, 1) # sort filtered list by "due" date and whether they are already marked as "done" agenda_items.sort( key=lambda x: (x.done, x.due_date) or (x.done, na_date)) # group report/done items by date for keys, groups in groupby( agenda_items, lambda x: ((x.due_date or na_date).year, (x.due_date or na_date).month, (x.due_date or na_date).day)): # filter out default dates again if (na_date.year, na_date.month, na_date.day) == keys: print(u"No done date attached") else: print(u"Agenda for {0:d}-{1:02d}-{2:02d}:".format(*keys)) for item in groups: print(" ", cr.render(item)) suppress_if_quiet( u"{nr} todo items displayed.".format(nr=len(agenda_items)), args)
def cmd_detach(tl, args): """detaches a file from a given todo item Required fields of :param:`args`: * item: the index number of the item from which something should be detached """ with ColorRenderer() as cr: item = tl.get_item_by_index(args.item) if not item: print(u"Could not find item '{item_id}'".format(item_id=args.item)) return attmnt_list = [] attmnt_list.extend(("url", url) for url in item.urls) for file_name in item.properties.get(conf.FILE, []): attmnt_list.append((conf.FILE, file_name)) if len(attmnt_list) == 0: print(u"This item has no file or URLs attached") quit(0) elif len(attmnt_list) == 1: attmnt = attmnt_list[0] if len(attmnt_list) > 1: print(u"Please choose one of the following attachments to delete:") for nr, attmnt in enumerate(attmnt_list): print(u" [{nr: 2d}] {attmnt}".format(nr=nr, attmnt=attmnt[1])) print(u" [x] Abort operation") answer = raw_input(u"Your choice: ").lower().strip() if answer == "x": quit(0) try: attmnt_nr = int(answer) attmnt = attmnt_list[attmnt_nr] except: print(u"Not a valid input") quit(0) if attmnt[0] == conf.FILE: item = tl.replace_or_add_prop(item, conf.FILE, None, attmnt[1]) else: item.text = u" ".join(item.text.replace(attmnt[1], "").split()) suppress_if_quiet(u" {item}".format(item=cr.render(item)), args) tl.dirty = True
def cmd_attach(tl, args): """attaches a file to the given todo item Required fields of :param:`args`: * item: the index number of the item to which something should be attached * location: either a (relative) file name or a (fully qualified) URL """ with ColorRenderer() as cr: item = tl.get_item_by_index(args.item) if not item: print(u"Could not find item '{item_id}'".format(item_id=args.item)) return if re_urls.match(args.location): # we got an URL suppress_if_quiet(u"Attaching URL {url}".format(url=args.location), args) item.text += u" {url}".format(url=args.location) item.urls.append(args.location.strip()) tl.dirty = True tl.reindex() else: # get path relative to todo file try: path = os.path.relpath(args.location, os.path.dirname(conf.todo_file)) except ValueError: # path is on other rive than reference location path = os.path.abspath(args.location) if not os.path.exists(path): print(u"File path '{fn}' does not exist".format(fn=path)) quit(-1) suppress_if_quiet(u"Attaching file {fn}".format(fn=path), args) tl.replace_or_add_prop(item, conf.FILE, path) tl.reindex() suppress_if_quiet(u" {item}".format(item=cr.render(item)), args)
def cmd_list(tl, args): """lists all items that match the given expression :description: If no search query is given, all items are listed. Required fields of :param:`args`: * search_string: a search string * all: if given, also the done todo and report items are shown * regex: if given, the search string is interpreted as a regular expression * ci: if given, the search string is interpreted as case insensitive """ with ColorRenderer() as cr: # case insensitivity if args.ci: flags = re.UNICODE | re.IGNORECASE else: flags = re.UNICODE # no search string given if not args.search_string: args.search_string = "." args.regex = True # given as regular expression if args.regex: re_search = re.compile(args.search_string, flags) else: re_search = re.compile(re.escape(args.search_string), flags) nr = 0 for item in tl.list_items(): if (not args.all) and (item.is_report or item.done): # if --all is not set, report and done items are suppressed #print(repr(item.properties)) continue if re_search.search(item.text): nr += 1 print(" ", cr.render(item)) suppress_if_quiet( u"{nr_items} todo items displayed.".format(nr_items=nr), args)
def cmd_context(tl, args): """lists all todo items per context Required fields of :param:`args`: * name: the name of the context to display * all: if given, also the done todo items are displayed * ci: if given, the context name is interpreted as case insensitive """ # lists todo items per context (like list, only with internal grouping) with ColorRenderer() as cr: # case insensitivity if args.ci: flags = re.UNICODE | re.IGNORECASE else: flags = re.UNICODE if args.name: args.name = re.escape(args.name) else: args.name = "." re_search = re.compile(args.name, flags) context_dict = collections.defaultdict(list) for item in tl.list_items(lambda x: True if args.all or not ( x.done or x.is_report) else False): for context in item.contexts: context_dict[context].append(item) #show context if the given name (partially) matches the context identifier args_list = [ name for name in sorted(context_dict) if re_search.search(name) ] nr = 0 for context in args_list: print(u"Context", cr.wrap_context(context, reset=True)) for item in sorted(context_dict[context], cmp=tl.default_sort): print(u" ", cr.render(item)) nr += 1 suppress_if_quiet(u"{nr} todo items displayed.".format(nr=nr), args)
def cmd_stop(tl, args): """stops working on a todo item without setting it to 'done'. :description: If a todo item is paused, the 'duration' property is updated and the 'started' property is removed. Required fields of :param:`args`: * item: the index number or id of the todo item which should be stopped """ with ColorRenderer() as cr: item = tl.get_item_by_index(args.item) if not item: print(u"No item found with number or ID '{item_id}'".format( item_id=args.item)) return if conf.STARTED not in item.properties: print(u"Todo item has not been started yet") return start_time = item.properties[conf.STARTED] now = datetime.datetime.now() time_delta = now - start_time duration = 0 try: # try to parse existing duration property duration = int(item.properties.get(conf.DURATION, 0)) except: pass # add delta time in minutes duration += int(time_delta.total_seconds() / 60) # remove started property tl.remove_prop(item, conf.STARTED, None) # update duration property tl.replace_or_add_prop(item, conf.DURATION, duration) suppress_if_quiet( u"You have worked {dur} minutes on:\n {item}".format( dur=duration, item=cr.render(item)), args)
def cmd_add(tl, args): """adds a new todo item to the todo list :description: The source of the todo item can either be the command line or an editor. Required fields of :param:`args`: * text: the text of the todo item to add """ with ColorRenderer() as cr: if not args.text: # no arguments given, open editor and let user enter data there output = get_editor_input("") item = tl.add_item( output.replace("\r\n", " ").replace("\n", " ").strip()) elif isinstance(args.text, list): # string not enclosed in "" item = tl.add_item(" ".join(args.text)) else: # single string item = tl.add_item(args.text) msg = u"Added {item}".format(item=cr.render(item)) suppress_if_quiet(msg, args) logger.debug(msg) item.check()
def cmd_report(tl, args): """shows a daily report of all done and report items :description: This command lists all done and report items for a given date or date range. If no arguments are given, the items of the last 7 days are displayed. Required fields of :param:`args`: * from_date: either a date or a string like 'tomorrow' or '*' * to_date: either a date or a string like 'tomorrow' """ with ColorRenderer() as cr: # default date used when no done date is specified na_date = datetime.datetime(1970, 1, 1, 0, 0, 0, 0) # today now = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) # check from and to date, make them datetime or None # what mode are we in? mode = None if args.from_date in ("*", "all"): mode, args.from_date, args.to_date = "ALL", na_date, now else: args.from_date = to_date(args.from_date) args.to_date = to_date(args.to_date) if isinstance(args.from_date, datetime.datetime): args.from_date = args.from_date.replace(hour=0, minute=0, second=0, microsecond=0) else: logger.debug( u"Cannot parse {date}".format(date=args.from_date)) args.from_date = None if isinstance(args.to_date, datetime.datetime): args.to_date = args.to_date.replace(hour=0, minute=0, second=0, microsecond=0) else: logger.debug(u"Cannot parse {date}".format(date=args.to_date)) args.to_date = None if args.from_date and args.to_date and not mode: mode = "RANGE" elif args.from_date and not args.to_date: mode, args.to_date = "DAY", args.from_date elif not mode: # last 7 days mode, args.from_date, args.to_date = "LASTWEEK", now - datetime.timedelta( days=7), now # swap dates, if necessary if args.from_date > args.to_date: args.from_date, args.to_date = args.to_date, args.from_date # set end date to end of day args.to_date = args.to_date.replace(hour=23, minute=59, second=59) logger.debug(u"Report mode {0}: from {1} to {2}".format( mode, args.from_date, args.to_date)) # get list of done and report items from current todo list report_list = list(tl.list_items(lambda x: (x.done or x.is_report))) # get list of done and report items from un-dated archive file root_dir = os.path.dirname(conf.todo_file) unsorted_fn = os.path.join(root_dir, conf.archive_unsorted_filename) if os.path.exists(unsorted_fn): res = TodoList(unsorted_fn) report_list.extend(res.todolist) # get all archive file names in list file_pattern = re_replace_archive_vars.sub( "*", conf.archive_filename_scheme) file_list = glob.glob(os.path.join(root_dir, file_pattern)) # regex for finding all replaced parts in archive filename scheme re_find_date_str = re_replace_archive_vars.sub( "(.+)", conf.archive_filename_scheme).replace("\\", "\\\\") re_find_date = re.compile(re_find_date_str, re.UNICODE) # loop through all files and see, whether they match the given date range for fn in file_list: # get all replaced values in filename parts = re_find_date.findall(fn)[0] # get the variables responsible for this substitution (e.archived_items. "%Y", "%m", ...) tvars = re_replace_archive_vars.findall( conf.archive_filename_scheme) # create mapping, removing duplicates mapping = dict(zip(tvars, parts)) # create date from mapping tdate = datetime.datetime.strptime(" ".join(mapping.values()), " ".join(mapping)) # if filename matches date range if args.from_date <= tdate <= args.to_date: # load todo list res = TodoList(fn) # get items directly if they are done or report items archived_items = [ item for item in res.todolist if item.done or item.is_report ] for item in archived_items: # replace id with (A) to mark it as archived item.replace_or_add_prop(conf.ID, "(A)") # append it to candidates report_list.extend(archived_items) # sort filtered list by "done" date report_list.sort(key=lambda x: x.done_date or na_date) nr = 0 # group report/done items by date for keys, groups in groupby( report_list, lambda x: ((x.done_date or na_date).year, (x.done_date or na_date).month, (x.done_date or na_date).day)): # we are looking at that date right now temp_date = datetime.datetime(year=keys[0], month=keys[1], day=keys[2]) # that date does not match the requested date range: skip if not args.from_date <= temp_date <= args.to_date: continue # filter out default dates again if is_same_day(na_date, temp_date): print(u"Report for unknown date:") else: print(u"Report for {date}:".format( date=temp_date.strftime("%A, %Y-%m-%d"))) # print the items, finally for item in groups: print(" ", cr.render(item)) nr += 1 suppress_if_quiet(u"{nr} todo items displayed.".format(nr=nr), args)