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_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_project(tl, args): """lists all todo items per project Required fields of :param:`args`: * name: the name of the project to display * all: if given, also the done todo items are displayed * ci: if given, the project name is interpreted as case insensitive """ # lists todo items per project (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) project_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 project in item.projects: project_dict[project].append(item) #show project if the given name (partially) matches the project identifier args_list = [name for name in sorted(project_dict) if re_search.search(name)] nr = 0 for project in args_list: print(u"Project", cr.wrap_project(project, reset=True)) for item in sorted(project_dict[project], cmp=tl.default_sort): nr += 1 print(" ", cr.render(item)) suppress_if_quiet(u"{nr} todo items displayed.".format(nr = nr), args)
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_remove(tl, args): """removes one or more items from the todo list """ 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_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_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_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_context(tl, args): """lists all todo items per context """ # 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_backup(tl, args): #@UnusedVariable """backups the current todo file to a timestamped file """ template = os.path.basename(conf.todo_file) filename = datetime.datetime.now().strftime("%Y-%m-%d_%H%M_" + template) # backup directory can be relative to todo file directory backup_folder = os.path.join(os.path.dirname(conf.todo_file), conf.backup_dir) # if not existing, create it if not os.path.exists(backup_folder): os.makedirs(backup_folder) # a filename has been specified by command line arguments if args.filename: filename = args.filename # destination file dst_fn = os.path.join(backup_folder, filename) if os.path.exists(dst_fn): # file already exists: what to do? if not confirm_action(u"File {fn} already exists. Overwrite (y/N) ".format(fn = dst_fn)): print(u" Aborting...") quit(0) else: print(u" Overwriting {fn}...".format(fn = dst_fn)) # copying the todo file to the destination with codecs.open(conf.todo_file, "r", "utf-8") as src: suppress_if_quiet(u" Copying todo file to {fn}...".format(fn = dst_fn), args) with codecs.open(dst_fn, "w", "utf-8") as dst: dst.write(src.read()) suppress_if_quiet(u"Successfully backed up todo file.", 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_list(tl, args): """lists all items that match the given expression """ 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_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_backup(tl, args): #@UnusedVariable """backups the current todo file to a timestamped file Required fields of :param:`args`: * filename: the name of the backup file [optional] """ template = os.path.basename(conf.todo_file) filename = datetime.datetime.now().strftime("%Y-%m-%d_%H%M_" + template) # backup directory can be relative to todo file directory backup_folder = os.path.join(os.path.dirname(conf.todo_file), conf.backup_dir) # if not existing, create it if not os.path.exists(backup_folder): os.makedirs(backup_folder) # a filename has been specified by command line arguments if args.filename: filename = args.filename # destination file dst_fn = os.path.join(backup_folder, filename) if os.path.exists(dst_fn): # file already exists: what to do? if not confirm_action( u"File {fn} already exists. Overwrite (y/N) ".format( fn=dst_fn)): print(u" Aborting...") quit(0) else: print(u" Overwriting {fn}...".format(fn=dst_fn)) # copying the todo file to the destination with codecs.open(conf.todo_file, "r", "utf-8") as src: suppress_if_quiet(u" Copying todo file to {fn}...".format(fn=dst_fn), args) with codecs.open(dst_fn, "w", "utf-8") as dst: dst.write(src.read()) suppress_if_quiet(u"Successfully backed up todo file.", args)
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_stop(tl, args): """stops working on a todo item without setting it to 'done' """ 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_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_done(tl, args): """sets the status of one or more todo items 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_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_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_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_delay(tl, args): """delays the due date of one or more todo items """ 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_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_agenda(tl, args): """displays an agenda for a given date """ 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_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_reopen(tl, args): """reopens one or more items marked as 'done' """ 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_overdue(tl, args): """shows all todo items that are overdue """ 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_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_archive(tl, args): """archives all non-current todo items and removes them from todo list moves all done / report items to other files (schema is specified in configuration) and removes them from the todo file. Required fields of :param:`args`: """ # base directory of todo file base_dir = os.path.dirname(conf.todo_file) # get list of done and report items report_list = list(tl.list_items(lambda x: x.done or x.is_report)) # default date used when no done date is specified na_date = datetime.datetime(1970, 1, 1) # sort filtered list by "done" date report_list.sort(key=lambda x: x.done_date or na_date, reverse=True) # for mapping items to file names file_map = collections.defaultdict(list) for item in report_list: item_date = item.done_date or na_date if is_same_day(item_date, na_date): dst_fn = conf.archive_unsorted_filename else: dst_fn = os.path.join( base_dir, item_date.strftime(conf.archive_filename_scheme)) # if not existing, create it if not os.path.exists(os.path.dirname(dst_fn)): os.makedirs(os.path.dirname(dst_fn)) # add to file map file_map[dst_fn].append(item) nr_archived = 0 # now we append the items to the right file for dst_fn in file_map: nr_archived += len(file_map[dst_fn]) # open files in append mode with codecs.open(dst_fn, "a", "utf-8") as fp: for item in file_map[dst_fn]: # and write them fp.write(item.text + "\n") # and remove the item from todo list tl.remove_item(item) suppress_if_quiet( u"Successfully archived {nr} todo items.".format(nr=nr_archived), args)
def cmd_repeat(tl, args): """marks the todo item as done and reenters it after the specified time """ 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_block(tl, args): """setting the first item as a pre-requisite to the second item """ 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 in blocked.properties.get(conf.BLOCKEDBY, []): print(u"Todo item '{item_id}' is already a pre-requisite of '{blocked_id}'.".format(item_id = item, blocked_id = blocked)) return tl.replace_or_add_prop(blocked, conf.BLOCKEDBY, item.tid) tl.clean_dependencies() tl.reindex() suppress_if_quiet(u" {item}".format(item = cr.render(blocked)), args)
def cmd_archive(tl, args): """archives all non-current todo items and removes them from todo list moves all done / report items to other files (schema is specified in configuration) and removes them from the todo file. Required fields of :param:`args`: """ # base directory of todo file base_dir = os.path.dirname(conf.todo_file) # get list of done and report items report_list = list(tl.list_items(lambda x: x.done or x.is_report)) # default date used when no done date is specified na_date = datetime.datetime(1970, 1, 1) # sort filtered list by "done" date report_list.sort(key=lambda x: x.done_date or na_date, reverse=True) # for mapping items to file names file_map = collections.defaultdict(list) for item in report_list: item_date = item.done_date or na_date if is_same_day(item_date, na_date): dst_fn = conf.archive_unsorted_filename else: dst_fn = os.path.join(base_dir, item_date.strftime(conf.archive_filename_scheme)) # if not existing, create it if not os.path.exists(os.path.dirname(dst_fn)): os.makedirs(os.path.dirname(dst_fn)) # add to file map file_map[dst_fn].append(item) nr_archived = 0 # now we append the items to the right file for dst_fn in file_map: nr_archived += len(file_map[dst_fn]) # open files in append mode with codecs.open(dst_fn, "a", "utf-8") as fp: for item in file_map[dst_fn]: # and write them fp.write(item.text + "\n") # and remove the item from todo list tl.remove_item(item) suppress_if_quiet(u"Successfully archived {nr} todo items.".format(nr = nr_archived), args)
def cmd_add(tl, args): """adds a new todo item to the todo list """ 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_age(tl, args): """listing items sorted by age (based on ``created`` property) """ with ColorRenderer() as cr: nr = 0 itemlist = [] for item in tl.list_items(): if not item.get_created_date(): # item does not have a creation date continue if (not args.all) and (item.is_report or item.done): # if --all is not set, report and done items are suppressed continue itemlist.append(item) nr += 1 for item in sorted(itemlist, key=attrgetter("created_date"), reverse=args.desc): print(" ", item.get_created_date().strftime("%Y-%m-%d %H:%M"), cr.render(item)) suppress_if_quiet(u"{nr_items} todo items displayed.".format(nr_items = nr), args) print(cmd_age.__description, cmd_age.__params)
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_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_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 """ 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_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_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_mark(tl, args): """lists all items with markers (e.g. '(!)') """ 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_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_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_edit(tl, args): """allows editing a given todo item """ 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_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_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_prio(tl, args): """assigns given items a priority (absolute like 'A' or relative like '-') """ 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_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_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)