def _do_lookup(self): """ Searches for and displays one or more log entries. Arguments: none. Returns: True if successful; False if the user aborted. ------------------------------------------------------------- """ try: # Clear the screen. wl_resource.print_header(self) # If there are no entries in the log, tell the user and then # return. if len(self.entries) == 0: io_utils.print_status( "Error", "There are no tasks in the log to search!", line_length=self.line_length) return False # end if # Main menu. option_list = [ "By Date/Time", "By Duration", "By Text Search", "By RE Pattern" ] prompt = "Please select a method by which to find entries:" search_opt = io_utils.menu(option_list, keystroke_list="#", prompt=prompt, line_length=self.line_length) # If the user quits, just return. if search_opt == 0: return False # end if # Otherwise call the appropriate lookup function. if search_opt == 1: results = wl_search.search_by_date(self) elif search_opt == 2: results = wl_search.search_by_duration(self) elif search_opt == 3: results = wl_search.search_by_text(self) else: # search_opt == 4 results = wl_search.search_by_re(self) # end if # If the search was unsuccessful, just return. if not results: return True # end if # Call the appropriate browse function, which will call other # functions if needed. if type(results[0]) == datetime.date: wl_search.select_date(self, results) return True else: wl_search.select_entry(self, results) return True # end if except Exception as err: _z_exc("worklog.py/WorkLog/_do_lookup", err)
def _do_close(self): """ Closes the work log object. The object will be overwritten by a new instance, or destroyed upon program exit. Arguments: none. Returns: nothing. ------------------------------------------------------------- """ try: if self.changed and io_utils.yes_no( f"{self.filename} has changed. Do you want to save it?", line_length=self.line_length): if not self._do_save(): io_utils.print_status("Error", "Error saving file.", line_length=self.line_length) # end if else: io_utils.print_status("Status", f"{self.filename} closed.", line_length=self.line_length) return False except Exception as err: _z_exc("worklog.py/WorkLog/do_close", err)
def _init_entries(self, entry_list): """ Initializes a set of log entries. Takes a list of dictionaries which have been read from a data file, uses them to initialize log entry objects and adds the log entry objects to the work log object. Arguments: - entry_list -- the list of OrderedDicts holding the log entries. Returns: the number of entries that could not be created. ------------------------------------------------------------- """ try: failed = 0 # Iterate through the list of entry dictionaries. for x, entry in enumerate(entry_list): # Try to intialize the entry. if not self._init_entry(entry): # If it didn't work, increment number of failed # entries. failed += 1 # Error message. io_utils.print_status("error", f"Failed to create entry #{x}.", go=True, line_length=self.line_length) # end if # end for # Return number of failed entries. return failed except Exception as err: _z_exc("worklog.py/WorkLog/_init_entries", err)
def calc_duration_rel(wl_obj, end_time, start_time): """ Determines a duration based on two times. Arguments: - wl_obj -- the work log object. - end_time -- the ending time. - start_time -- the starting time. Returns: a timedelta object representing the difference between the two times, if that difference is positive; else None. ----------------------------------------------------------------- """ try: # Subtract the start time from the end time. (Note that if a # task extends across more than one day, the duration must be # entered directly.) if end_time >= start_time: # Create timedeltas for subtraction. end_td = datetime.timedelta( hours=end_time.hour, minutes=end_time.minute) start_td = datetime.timedelta( hours=start_time.hour, minutes=start_time.minute) # Return the resulting timedelta. return end_td - start_td else: msg = "The end time cannot be before the start time." io_utils.print_status("Error", msg, line_length=wl_obj.line_length) return None # end if except Exception as err: _z_exc("wl_datetime/calc_duration_rel", err)
def _do_open(self): """ Opens a log file and reads data into the log object. Arguments: none. Returns: True if successful; False if there was an error. ------------------------------------------------------------- """ try: # If we don't have a filename, get one. if self.filename == "": self.filename = io_utils.get_filename_open(filetype="csv") # end if # If self.filename is still empty, the user chose to go # back, so just return. if self.filename == "": return False # end if # Open and read the file. entry_dict = io_utils.file_read(self.filename, filetype="csv") # If the file didn't open properly, let the user know before # returning. if not entry_dict: io_utils.print_status("Error", f"{self.filename} could not be opened.") return False # end if # Pop the first entry in the list of dictionaries that was # read from the file and use it to initialize the work log # object. Return False if the operation fails. if not self._init_worklog(entry_dict.pop(0)): return False # end if # Initialize the log entry objects. failed = self._init_entries(entry_dict) # Once all the entries have been added, sort the lists. self.sorts[TITLE_SORT].sort() self.sorts[DATE_SORT].sort() # Print final status. msg = f"{self.filename} opened. {len(self.entries)} entries read." if failed: msg += ( f" {self.total_entries - len(self.entries)} entries " + "not read. The file may be corrupted.") elif self.total_entries != len(self.entries): msg += ( f" {self.total_entries} entries expected. The file may " + "have been edited outside the Work Log program.") io_utils.print_status("Status", msg, line_length=self.line_length) # Finally reset the total_entries attribute to the actual # number of entries added. self.total_entries = len(self.entries) return True except Exception as err: _z_exc("worklog.py/WorkLog/_do_open", err)
def _do_save(self): """ Saves the log file. Allows the user to continue working with the log object. Arguments: none. Returns: True if successful; False if there was an error. ------------------------------------------------------------- """ try: self.last_modified = datetime.datetime.now() # Initialize a temporary list to hold the log data. entry_list = [] # Create a dummy entry object, which will hold data for the # log object. new_entry = logentry.LogEntry() fn = new_entry.FIELDNAMES new_entry.info = { "total_entries": self.total_entries, "date_format": self.date_format, "time_format": self.time_format, "show_help": self.show_help, "last_modified": self.last_modified } # Start the list with the dummy entry object. entry_dict = new_entry.to_dict() entry_list.append(entry_dict) # Now add all of the entries. for entry in self.entries: # For each entry object, convert to a dictionary. entry_dict = entry.to_dict() entry_list.append(entry_dict) # end for # Pass everything to the io_utils function. success = io_utils.file_write(self.filename, "csv", entry_list, fieldnames=fn) if success: # Print status. io_utils.print_status("status", f"{self.filename} saved.", line_length=self.line_length) self.changed = False return True else: return False # end if except Exception as err: _z_exc("worklog.py/WorkLog/_do_save", err)
def set_endian(wl_obj): """ Allows the user to set the preferred date format. Arguments: - wl_obj -- the work log object. Returns: nothing ----------------------------------------------------------------- """ try: # Endian examples. end_list = [ "July 15, 2010 (7/15/10)", "15 July, 2010 (15/7/10)", "2010 July 15 (10/7/15)"] # Print explanation. if wl_obj.date_format: msg = "The current date format is " if wl_obj.date_format == "M": msg += end_list[0] elif wl_obj.date_format == "L": msg += end_list[1] else: # wl_obj.date_format == "B" msg += end_list[2] # end if else: msg = "The date format has not been set." # end if io_utils.print_status( "Status", msg, go=True, line_length=wl_obj.line_length) # If a date format is already set, allow the user to leave it. if wl_obj.date_format: q = True else: q = False # end if # Ask the user to set a date format. response = io_utils.menu( end_list, keystroke_list="#", confirm=True, quit_=q, prompt="Please select your preferred date format:") if response == 0: return elif response == 1: wl_obj.date_format = "M" elif response == 2: wl_obj.date_format = "L" else: # response == 3 wl_obj.date_format = "B" # end if return except Exception as err: _z_exc("wl_datetime/set_endian", err)
def _find_entries_re(wl_obj, pattern, fields): """ Finds entries matching a regular expression. Arguments: - wl_obj -- the work log object. - pattern -- the regular expression to search. - fields -- the field(s) to search. Returns: a list of matching entries, or an empty list if no matches were found. ----------------------------------------------------------------- """ try: return_list = [] # If the user has specified a case-insensitive search-- # Make sure the pattern is valid. if re.search(r", re\.I", pattern): # Slice off the flag.1 pattern = pattern[:-6] try: pattern = re.compile(pattern, re.I) except Exception: io_utils.print_status("Error", f"{pattern} failed to compile.", go=True) return [] # end try else: try: pattern = re.compile(pattern) except Exception: io_utils.print_status("Error", f"{pattern} failed to compile.", go=True) return [] # end try # end if for entry in wl_obj.entries: if (((fields in [TITLE, BOTH]) and (re.search(pattern, entry.title))) or ((fields in [NOTES, BOTH]) and (re.search(pattern, entry.notes)))): return_list.append(entry) # end if # end for return return_list except Exception as err: _z_exc("wl_search.py/find_entries_re", err)
def set_screen_width(wl_obj): """ Resets the width of the screen in characters. Arguments: - wl_obj -- the work log object. Modifies: -- the work log object's line_length attribute. Returns -- nothing. ----------------------------------------------------------------- """ try: # Display current width. io_utils.print_status( "Status", f"The current width of the screen is {wl_obj.line_length} " + "characters.", go=True, line_length=wl_obj.line_length) msg = ("Please enter your desired screen width (must be at least 40 " + "characters), or press [ENTER] to go back:") # Loop until user enters a valid length or quits. while True: # Get the new line length (must be >= 40). response = io_utils.get_input(msg, typ="int", line_length=wl_obj.line_length, must_respond=False) # If the user quits, leave line_length unchanged. if not response: return elif (response < 40): io_utils.print_status("Error", "You did not enter a valid number.", line_length=wl_obj.line_length) if io_utils.yes_no("Try again?", line_length=wl_obj.line_length): continue else: return # end if else: wl_obj.line_length = response return # end if # end while except Exception as err: _z_exc("wl_resource.py/set_screen_width", err)
def _add_entry(self, entry, recurring_entries): """ Adds one entry. Arguments: - entry -- the entry to add. - recurring_entries -- the list of recurring entries, if there are any. Returns: True if the user wants to add another task, else False. ------------------------------------------------------------- """ try: # Set the datetime attribute. entry.datetime = wl_add.add_datetime(entry) # Add the entry to the work log. self._do_sort(entry) self.entries.append(entry) # Now add the recurring entries, if any. self._add_recurring_entries(entry, recurring_entries) # And update. self.total_entries = len(self.entries) # Note the log object has changed. self.changed = True # Build status message. if recurring_entries: msg = f"{len(recurring_entries) + 1} entries added." else: msg = "Entry added." # end if # Update the total entries attribute. self.total_entries = len(self.entries) # Print that the entry was added. io_utils.print_status("Status", msg, line_length=self.line_length) # Finally, ask the user if they want to add another # entry. (Whether they are done will be the opposite # of their answer.) return io_utils.yes_no("Do you want to add another task?", line_length=self.line_length) except Exception as err: _z_exc("worklog.py/WorkLog/do_add", err)
def _init_worklog(self, dict_entry): """ Initializes the work log object from data from a file. The first log entry read from a data file does not contain information about a task; rather, its info attribute holds data on the work log itself. Arguments: - dict_entry -- the list representation of the first log entry as read from a data file. Returns: True if data was successfully transferred to the work log object; else False. ------------------------------------------------------------- """ try: # Create a dummy log entry object to hold the data. new_entry = logentry.LogEntry() # Convert the log object information (stored in the info # field of the first entry obuject) into a dictionary. new_entry.from_dict(dict_entry) # Set the log object's attributes from the dictionary. self.total_entries = new_entry.info["total_entries"] self.show_help = new_entry.info["show_help"] self.last_modified = new_entry.info["last_modified"] # But only set the format attributes if the user has not # already set them. if not self.date_format: self.date_format = new_entry.info["date_format"] # end if if not self.time_format: self.time_format = new_entry.info["time_format"] # end if return True except Exception as err: io_utils.print_status("Error", f"Error reading log info: {err}", line_length=self.line_length) return False
def set_time_format(wl_obj): """ Allows the user to set the preferred time format. Arguments: - wl_obj -- the work log object. Returns: nothing ----------------------------------------------------------------- """ try: # Build message. if not wl_obj.time_format: msg = "The time format has not been set." elif wl_obj.time_format == 12: msg = "The current time format is 12-hour (AM/PM)." else: # wl_obj.time_format == 24 msg = "The current time format is 24-hour (Military Time)." # end if # Print status. io_utils.print_status( "Status", msg, go=True, line_length=wl_obj.line_length) # Display menu and get response. response = io_utils.menu( ["12-hour Clock (AM/PM)", "24-hour Clock (Military Time)"], keystroke_list="#") # If the user chose to quit, just return without changing # anything. if response == 0: return # Else set the time_format attribute to the user's choice. elif response == 1: wl_obj.time_format = 12 else: # response == 2 wl_obj.time_format = 24 # end if return except Exception as err: _z_exc("wl_datetime/set_time_format", err)
def _do_create(self): """ Creates a new file to store data from the log object. If the user names an existing file, offers to open that file. Arguments: none. Returns: True if successful; False if there was an error or the user aborts the process. ------------------------------------------------------------- """ try: # Call the io_utils method to get a filename and create the file. self.filename, go_open = io_utils.file_create(filetype="csv") # If no filename was returned, the attempt was unsuccessful. # Return False if self.filename == "": return False # If the open flag was set, call _do_open for the file. elif go_open: success = self._do_open() if success: return True else: return False # end if else: self.total_entries = 0 # Print status. io_utils.print_status("Status", f"{self.filename} created.", line_length=self.line_length) return True # end if except Exception as err: _z_exc("worklog.py/WorkLog/do_create", err)
def search_by_re(wl_obj): """ Finds work log entries based on a regular expression. Arguments: - wl_obj -- the work log object. Returns: a list of matching entries, if any are found; else an empty list. ----------------------------------------------------------------- """ try: # Run everything inside a loop in case the user wants to start # over. while True: wl_obj.help.print_help(wl_obj.show_help, "RE Search", "_sh_re", line_length=wl_obj.line_length) # Get a regex string. re_string = io_utils.get_input( "Enter a regular expression (without quotation marks):") # If the user chose to toggle help, do that. if re.match(r"-h", re_string, re.I): wl_obj.show_help = not (wl_obj.show_help) continue # end if # If the user didn't enter anything... if not re_string: io_utils.print_status("Error", "You did not enter anything.", go=True, line_length=wl_obj.line_length) if not io_utils.yes_no("Try again?", line_length=wl_obj.line_length): return [] else: continue # end if # end if # Option menu. search_mode = io_utils.menu( ["Title", "Notes", "Title and Notes"], keystroke_list="#", prompt="Which field(s) would you like to search?", line_length=wl_obj.line_length) # User quits, just exit. if search_mode == QUIT: return None # end if # Get the results of the find function. return_list = _find_entries_re(wl_obj, re_string, search_mode) # If nothing matched, tell the user. if len(return_list) == 0: io_utils.print_status("Status", "No matches found.", line_length=wl_obj.line_length) # end if return return_list # end if # end while except Exception as err: _z_exc("wl_search.py/search_by_re", err)
def search_by_text(wl_obj): """ Finds work log entries based on a text string. Arguments: - wl_obj -- the work log object. Returns: a list of matching entries, if any are found; else an empty list. ----------------------------------------------------------------- """ try: # Run everything inside a loop in case the user wants to start # over. while True: # Option menu. search_fields = io_utils.menu( ["Title", "Notes", "Title and Notes"], keystroke_list="#", prompt="Which field(s) would you like to search?", line_length=wl_obj.line_length) # User quits, just exit. if search_fields == QUIT: return None # end if # Print the instructions. wl_obj.help.print_help(wl_obj.show_help, "Search Terms", "_sh_text", line_length=wl_obj.line_length) # Get a text string to search for. search_string = io_utils.get_input("Enter text to search for:") # If the user chose to toggle help, do that. if re.match(r"-h", search_string, re.I): wl_obj.show_help = not (wl_obj.show_help) continue # If the user didn't enter anything, either loop back or # return. if not search_string: print("You did not enter anything.") if not io_utils.yes_no("Try again?", line_length=wl_obj.line_length): return [] else: continue # end if # end if # If the search string includes wildcard characters, ask the # user how to treat them. if re.search(r"[?*]", search_string): search_mode = io_utils.menu( [ "Literal search (? and * will only match themselves)", "Wildcard search (? and * will match any character)" ], prompt="Your search string contains one or more wildcard " + "characters (? or *). What kind of search would you " + "like to perform?", keystroke_list="#", line_length=wl_obj.line_length) # If the user chooses to go back, start over. if search_mode == QUIT: continue # end if # Set the default to literal search. else: search_mode = 1 # Return the results of the find function. return_list = _find_entries_text(wl_obj, search_string, search_fields, search_mode) # If nothing matched, tell the user. if len(return_list) == 0: io_utils.print_status("Status", "No matches found.", line_length=wl_obj.line_length) # end if return return_list # end while except Exception as err: _z_exc("wl_search.py/search_by_text", err)
def select_date(wl_obj, date_list): """ Show a list (or part of a list) of dates that contain log entries. Arguments: - wl_obj -- the work log object. - date_list -- the list of dates. Returns: nothing. ----------------------------------------------------------------- """ try: start = 0 # Clear the screen. wl_resource.print_header(wl_obj) # Print the number of dates found. if len(date_list) == 1: msg = "Found 1 date with entries." else: msg = f"Found {len(date_list)} dates with entries." # end if io_utils.print_status("Status", msg, line_length=wl_obj.line_length) # Run in a loop until the user is done viewing/editing, then # return. while True: # Call list browse. response = wl_viewedit.browse_list(wl_obj, date_list, start=start) # If the response isn't an integer, it's a command to move # forward or back. Move the start position, but only if it # doesn't go beyond the bounds of the list. if type(response) == str: if (response.upper() == "P") and (start - 9 >= 0): start -= 9 elif ((response.upper() == "N") and (start + 9 < len(date_list))): start += 9 # end if # Clear the screen before looping back. wl_resource.print_header(wl_obj) continue # end if # If the user quits, return. if response == QUIT: return # end if # If it's a non-zero integer, the user chose a date. Find # all entries on that date. start_date = datetime.datetime.combine( date_list[start + response - 1], datetime.time()) end_date = start_date.replace(hour=23, minute=59) entry_list = _find_entries_date(wl_obj, start_date, end_date) # Let the user browse/edit those entries. (If the user # edited the list, it will come back changed.) entry_list = wl_viewedit.browse_entries(wl_obj, entry_list) # If the user deleted all of the entries for a date, then # that date is no longer valid and must be removed. if len(entry_list) == 0: del date_list[start + response - 1] # end if # Clear the screen before looping back. wl_resource.print_header(wl_obj) # end while except Exception as err: _z_exc("wl_search.py/select_date", err)
def browse_list(wl_obj, browse_list, start=0): """ Presents a list to the user and asks him/her to select one. Arguments: - wl_obj -- the work log object. - browse_list -- the list to present. - start -- the first item to display (default 0). Returns: an integer representing an item to display, 0 if the user quits, or a string representing a navigational command. ----------------------------------------------------------------- """ try: # If the list is empty, automatically return as if the user had # quit. if len(browse_list) == 0: return 0 # end if # Clear the screen and print the matches to be listed. wl_resource.print_header(wl_obj) if len(browse_list) == 1: msg = "Showing match 1 of 1" else: msg = f"Showing matches {start + 1}-" if start + 9 <= len(browse_list): msg += f"{start + 9} of {len(browse_list)}" else: msg += f"{len(browse_list)} of {len(browse_list)}" # end if # end if io_utils.print_status("Status", msg, go=True, line_length=wl_obj.line_length) # Build the options list. options = [] # For dates, just append the date (as a string). if type(browse_list[0]) == datetime.date: for ndx in range(start, start + 9): # Stop if at the end of the list. if ndx == len(browse_list): break # end if options.append(str(browse_list[ndx])) # end for # For entries, append the title, date and time (as strings). else: for ndx in range(start, start + 9): # Stop if at the end of the list. if ndx == len(browse_list): break # end if # Gather the fields. title = browse_list[ndx].title date = wl_resource.format_string(wl_obj, browse_list[ndx].date, short=True) time = wl_resource.format_string(wl_obj, browse_list[ndx].time) # If the time had a leading zero stripped, replace it # with a space. if time[1] == ":": time = f" {time}" # end if max_title_len = wl_obj.line_length - 23 # If the title is too long to fit in the column, # truncate it. if len(title) > max_title_len: title = title[:max_title_len - 1] + "… " else: title += " " # end if rj = wl_obj.line_length - len(title) - 4 option = title + f" {date} {time}".rjust(rj, ".") # Append the assembled string. options.append(option) # end for # end if if start > 0: prev = True else: prev = False # end if if start + 9 < len(browse_list): nxt = True else: nxt = False # end if # Now display the menu and return the user's choice. return io_utils.menu(options, prompt="Select an entry to view:", keystroke_list="#", nav=True, prev=prev, nxt=nxt, line_length=wl_obj.line_length) except Exception as err: _z_exc("wl_viewedit.py/browse_list", err)
def select_entry(wl_obj, entry_list): """ Allows the user to either browse a set of entries, or choose from a list. Arguments: - wl_obj -- the work log object. - entry_list -- the list of entries. Returns: nothing. ----------------------------------------------------------------- """ try: # Clear the screen. wl_resource.print_header(wl_obj) # Print the number of dates found. if len(entry_list) == 1: msg = "Found 1 task." else: msg = f"Found {len(entry_list)} tasks." # end if io_utils.print_status("Status", msg, line_length=wl_obj.line_length) # Ask the user to browse or pick from list. response = io_utils.menu( [ "Browse all matching tasks", "Choose from a list of matching tasks" ], keystroke_list="#", quit_=True, prompt="Please select how you want to see the matching tasks:", line_length=wl_obj.line_length) # If the user chooses to go back, just return. if response == QUIT: return # end if # If the user chooses to browse the matches, call the browse # function, then return. if response == 1: entry_list = wl_viewedit.browse_entries(wl_obj, entry_list) return # If the user chooses to see a list, display it here. else: start = 0 # Run in a loop until the user is done viewing/editing, then # return. while True: # Call list browse. response = wl_viewedit.browse_list(wl_obj, entry_list, start=start) # If the response isn't an integer, it's a command to # move forward or back. Move the start position, but # only if it doesn't go beyond the bounds of the list. if type(response) == str: if (response.lower() == "p") and (start - 9 >= 0): start -= 9 elif ((response.lower() == "n") and (start + 9 < len(entry_list))): start += 9 # end if # Clear the screen before looping back. wl_resource.print_header(wl_obj) continue # end if # If the user quits, return. if response == QUIT: return # end if # If it's a non-zero integer, the user chose an entry. # Get the index number of the chosen entry in the list. ndx = start + response - 1 # Browse entries, starting with the selected one. wl_viewedit.browse_entries(wl_obj, entry_list, ndx=ndx) # Clear the screen before looping back. wl_resource.print_header(wl_obj) # end while # end if except Exception as err: _z_exc("wl_search.py/select_entry", err)
def _get_time(wl_obj, t_type, date, start=None): """ Gets a time from the user. Arguments: - wl_obj -- the work log object. - t_type -- which date is being modified (single, start, end) - date -- the date object to which to append the time. Keyword Arguments: - start -- the starting date/time (for error checking). Returns: 1) an integer representing the return state: -1 if the user chose to go back, 0 if the user chose to abort completely, 1 if the user successfully entered a date; 2) if the user enters a valid time, a datetime object with the original date combined with the time; otherwise, the original date object. ----------------------------------------------------------------- """ try: # Prompt. if t_type == "single": t_type = "conduct" # end if # Build prompt. prompt = ( f"If you would like to {t_type} your search at a specific time" + f" during {wl_resource.format_string(wl_obj, date, short=True)}, " + "enter the time now, or just press [ENTER] to include the entire " + "day:") # Loop until a valid response is obtained. while True: # Get a time. response = io_utils.get_input(prompt, must_respond=False) # If the user didn't enter anything, just return the date. # (Since the caller will set the time to 11:59pm, it will # never be earlier than the starting time.) if not response: return 1, date # end if # Return None if the user wants to quit. if response.lower() == "-b": return -1, None # end if # Otherwise parse the input for a time. time = wl_datetime.parse_time_input(wl_obj, response) msg = f"{response} could not be interpreted as a valid time." # If a time was found, combine it with the date and return, # unless-- if time: # If this is the start date, or if this is the end date # and it is different than the start date, then there # is no need to check the time; just return the # combined datetime object. if (start is None) or (date != start.date()): return 1, datetime.datetime.combine(date, time) # But if the start and end dates are the same, the end # time cannot precede the start time. else: if time >= start.time: return 1, datetime.datetime.combine(date, time) else: msg = ( f"The end time cannot be earlier than {start.time}" ) # end if # end if # end if # Print an error message, ask the user if they want to try # again, and if they say no, return None (else loop back). io_utils.print_status("Error", msg, line_length=wl_obj.line_length) retry = io_utils.yes_no("Try again?", quit_=True, line_length=wl_obj.line_length) if retry in ["-b", False]: return -1, None elif retry == "-q": return 0, None # end if # end while except Exception as err: _z_exc("wl_search.py/_get_time", err)
def _do_add(self): """ Adds one or more entry objects to the log object. Arguments: none. Returns: nothing. ------------------------------------------------------------- """ try: # List of object attributes to set (easier than directly # iterating through the attributes). attr_list = [ "title", "date", "time", "duration", "notes", "recurring" ] # Initialize list. recurring_entries = [] # Like all of the _do methods, the entire method is inside a # loop, allowing the user to add as many entries as they # want. not_done = True while not_done: cancel = False # First, create a new log entry object. new_entry = logentry.LogEntry() # Go through the list and set values for each attribute. # Because the user can choose to back up, we don't use # a for loop to iterate through the attributes. attrib = 0 # Loop runs until the last attribute is set. while attrib < len(attr_list): # Get the attribute name. attr = attr_list[attrib] # The title attribute is the only one which does not # allow the user to go back (because it's the first # attribute set). if attr == "title": go = wl_add.add_title(self, new_entry) # end if # "date" must be convertible to a date object. elif attr == "date": go = wl_add.add_date(self, new_entry) # If the user goes back a step, clear the # previous attribute. if go == -1: new_entry.title = None # end if # "time" must be convertible to a time object. elif attr == "time": go = wl_add.add_time(self, new_entry) # If the user goes back a step... if go == -1: new_entry.date = None # end if # "duration" must be convertible to a timedelta # object. elif attr == "duration": go = wl_add.add_duration(self, new_entry) # If the user goes back a step... if go == -1: new_entry.time = None # end if # "notes" can be any string, including empty. elif attr == "notes": go = wl_add.add_note(self, new_entry) # If the user goes back a step... if go == -1: new_entry.duration = None # end if # "recurring" will either be True of False, and if # True will also return a list of recurrance date # objects. elif attr == "recurring": recurring_entries = [] go, recurring_entries = wl_add.add_recurrance( self, new_entry) # If the user goes back a step... if go == -1: new_entry.notes = None # end if # end if if go == 0: # Print that the entry was cancelled. io_utils.print_status( "Status", "Addition of this task has been cancelled.", line_length=self.line_length) # If the user aborts and does not want to start # another task, return immediately. if io_utils.yes_no("Do you want to add another task?", line_length=self.line_length): cancel = True break else: return # end if else: attrib += go # end if # end while if not cancel: not_done = self._add_entry(new_entry, recurring_entries) # end if # end while return except Exception as err: _z_exc("worklog.py/WorkLog/do_add", err)
def _get_date(wl_obj, d_type, start=None): """ Gets a date from the user. Arguments: - wl_obj -- the work log object. - d_type -- which date to prompt for (single, start, end). Keyword Arguments: - start -- starting date (for error checking). Returns: 1) an integer representing the return state: -1 if the user chose to go back, 0 if the user chose to abort completely, 1 if the user successfully entered a date; 2) a date object, or None if the user chose to quit or go back. ----------------------------------------------------------------- """ try: # Set the prompt. if d_type == "single": prompt = "Enter the date on which to search:" elif d_type == "start": prompt = "Enter the starting date for your search:" else: prompt = "Enter the ending date for your search:" # end if # Loop until a valid date is obtained or the user aborts. while True: # Get the date. response = io_utils.get_input(prompt) # Error prompt. msg = "You did nol enter anything." # If the user entered something, parse it. if response: date = wl_datetime.parse_date_input(wl_obj, response) if date: if d_type != "end": return 1, date else: if date >= start.date(): return 1, date elif date and date < start.date(): # Alternate error message. msg = ("The end date cannot be ealier than " + f"{start.date}") io_utils.print_status( "Error", msg, line_length=wl_obj.line_length) # end if # end if # end if retry = io_utils.yes_no("Try again?", quit_=True, line_length=wl_obj.line_length) # If the user decides not to try again, return. (Not trying # again here is treated as equivalent to going back.) if retry in ["-b", False]: return -1, None elif retry == "-q": return 0, None # end if # end while except Exception as err: _z_exc("wl_search.py/_get_date", err)
def search_by_duration(wl_obj): """ Finds work log entries based on a duration or duration range. Arguments: - wl_obj -- the work log object. Returns: a list of matching entries, if any are found; else an empty list. ----------------------------------------------------------------- """ try: # Run everything inside a loop in case the user wants to start # over. while True: # Option menu. search_mode = io_utils.menu( ["A specific duration", "A range of durations"], keystroke_list="#", prompt="What would you like to search?", line_length=wl_obj.line_length) # User quits, just exit. if search_mode == QUIT: return None # end if # Both single duration and range require one duration, so # get one now. if search_mode == DURATION: d_type = "single" else: d_type = "minimum" # end if go, min_duration = _get_duration(wl_obj, d_type) # If the user wants to go back, loop back to the beginning. if go == GO_BACK: continue # If the user aborts, return. elif go == QUIT: return [] # end if # If the user wants to search a range, get a maximum # duration. if search_mode == DURATION_RANGE: d_type = "maximum" go, max_duration = _get_duration(wl_obj, d_type, min_duration) # Again, loop back or return if the user chooses to go # back or abort. if go == GO_BACK: continue elif go == QUIT: return [] # end if # If the search is for a single duration, just set the # maximum duration to equal the minimum. else: max_duration = min_duration # end if # Now find all entries that match. r_list = _find_entries_duration(wl_obj, min_duration, max_duration) # If no matches, tell the user. if len(r_list) == 0: io_utils.print_status("Status", "No matches found.", line_length=wl_obj.line_length) # end if return r_list # end if # end while except Exception as err: _z_exc("wl_search.py/search_by_duration", err)
def parse_date_phrase(wl_obj, string): """ Function that checks a string to see if it contains a valid word date. Arguments: - wl_obj -- the work log object. - string -- the user input. Returns: a date object if successful, or None. ----------------------------------------------------------------- """ try: # First break the string into a list of words. word_list = re.findall(r"\b\w+\b", string) # If the user included the words "the" or "of", discard it. s = [] for word in word_list: if word.lower() != "the" and word.lower() != "of": s.append(word) # end if # end for word_list = s # Check according to the length of the string. if len(word_list) == 1: # There are three valid one-word responses, plus the days of # the week. if word_list[0].lower() == "today": return datetime.date.today() elif word_list[0].lower() == "yesterday": return datetime.date.today() - datetime.timedelta(days=1) elif word_list[0].lower() == "tomorrow": return datetime.date.today() + datetime.timedelta(days=1) else: valid = wl_resource.weekday(word_list[0]) # If the string is a day of the week, return the date # object that corresponds to that day of the current # week. Else return None. if valid: return _create_date_from_weekday(valid, 0) else: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if # end if elif len(word_list) == 2: # A two-word response can be a calendar date without the # year, or a phrase. Check for a calendar date first. good = parse_date_calendar(wl_obj, word_list) if good: return good # end if # Two-word date phrases all start with "this", "next", or # "last", followed by a day of the week. if word_list[0].lower() == "this": offset = 0 elif word_list[0].lower() == "last": offset = -1 elif word_list[0].lower() == "next": offset = 1 else: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if valid = wl_resource.weekday(word_list[1]) if valid: return _create_date_from_weekday(valid, offset) else: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if elif len(word_list) == 3: # Three word calendar dates are a full month, day and year # (but not necessarily in that order). Check for them. good = parse_date_calendar(wl_obj, word_list) if good: return good # end if # There are two set three-word date phrases. It's simpler # to search for them in the original string. if re.search(r"day after tomorrow", string, re.I): return datetime.date.today() + datetime.timedelta(days=2) elif re.search(r"day before yesterday", string, re.I): return datetime.date.today() - datetime.timedelta(days=2) # end if # Other three-word date phrases are a day of the week # followed by either "before last" or "after next". valid = wl_resource.weekday(word_list[0]) if valid: if ( word_list[1].lower() == "before" and word_list[2].lower() == "last"): return _create_date_from_weekday(valid, -2) elif ( word_list[1].lower() == "after" and word_list[2].lower() == "next"): return _create_date_from_weekday(valid, 2) else: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if else: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if else: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if except Exception as err: _z_exc("wl_datetime/parse_date_phrase", err)
def parse_date_numeric(wl_obj, string): """ Validates a numeric date in the current format. Allows the date format to be changed if the numeric combination is a valid date in a different format. Arguments: - wl_obj -- the work log object. - string -- the user input. Returns: a date object if successful, or None. ----------------------------------------------------------------- """ try: # Format constants. BIG_FORMAT = "%Y %B %d" MID_FORMAT = "%B %d, %Y" LIT_FORMAT = "%d %B %Y" # First separate the elements of the date and convert them to # integers. numbers = re.findall(r"\d+", string) for x, number in enumerate(numbers): numbers[x] = int(number) # end for # If there are only two elements, the year was omitted and # defaults to the current year. Where the year element is # inserted depends on the format. if len(numbers) == 2: if wl_obj.date_format == "B": numbers.insert(0, datetime.date.today().year) else: numbers.append(datetime.date.today().year) # end if # end if # Now try to create a date object with the selected format. entry_date = _create_date(numbers, wl_obj.date_format) # If it's valid, return it. if entry_date: return entry_date # If it's not valid, try the other formats. valid_formats, valid_dates = _check_other_endians( numbers, wl_obj.date_format) # If neither of the other formats is valid, just return None. if valid_formats == []: io_utils.print_status( "Error", f"{string} could not be interpreted as a valid date.", line_length=wl_obj.line_length) return None # end if # Otherwise, inform the user of the successful format(s) and ask # if he/she wants to change the selected format to match, or # re-enter the date. msg = io_utils.print_block( f"The date {string} is not valid in the currently selected " + "format, but is valid in a different format. You can choose to " + "change the date format or re-enter the date for this task.", ret_str=True) io_utils.print_status("Warning", msg, line_length=wl_obj.line_length) option_list = [] for x, ndn in enumerate(valid_formats): if ndn == "B": option_list.append(valid_dates[x].strftime(BIG_FORMAT)) elif ndn == "M": option_list.append(valid_dates[x].strftime(MID_FORMAT)) else: option_list.append(valid_dates[x].strftime(LIT_FORMAT)) # end if # end if # end for option_list.append("Re-enter the date.") # Give the user the option to change the format or try again. response = io_utils.menu(option_list, keystroke_list="#", quit_=False) # If the user chose to try again, return None. if response == len(option_list): input("Press [ENTER] to continue.") return None # Otherwise, reset the date format and return the date object. else: wl_obj.date_format = valid_formats[response - 1] return valid_dates[response - 1] # end if except Exception as err: _z_exc("wl_datetime/parse_date_numeric", err)
def search_by_date(wl_obj): """ Finds work log entries based on a date/time or date/time range. Arguments: - wl_obj -- the work log object. Returns: for searches by date/time, a list of matching entries from the date-sorted index, if any are found, an empty list if no matches are found, or None if the user aborts; for a view of all dates, a list of unique date objects. ----------------------------------------------------------------- """ try: # Run everything inside a loop in case the user wants to start # over. while True: # Option menu. search_mode = io_utils.menu([ "A single date/time", "A range of dates/times", "View all dates" ], keystroke_list="#", prompt="How would you like to search?", line_length=wl_obj.line_length) # User quits, just exit. if search_mode == QUIT: return None # end if if search_mode in [DATE, DATE_RANGE]: # Both date/time searches require at least one date. if search_mode == DATE: d_type = "single" else: d_type = "start" # end if # Get the first (and for a single date/time search, # only) date/time. go, date = _get_date(wl_obj, d_type) # If the user decided to go back here, loop back. if go == GO_BACK: continue # end if # If the user decided to abort, confirm and then return. if go == QUIT and io_utils.confirm("abort the search"): return None # end if # Now get a time, if the user wants one. go, date = _get_time(wl_obj, d_type, date) # If the user decided to go back here, loop back. if go == GO_BACK: continue # end if # If the user decided to abort, confirm and then return. if go == QUIT and io_utils.confirm("abort the search"): return None # end if # If the user declined to enter a time, set the time of # the datetime object to midnight. if type(date) == datetime.date: start_date = datetime.datetime.combine( date, datetime.time()) # Otherwise the datetime object is already set. else: start_date = date # end if # If the user wants to search a single date/time... if search_mode == DATE: # If the user wants to search a specific date AND # time, set the end of the range to equal the # start. if type(date) == datetime.datetime: end_date = start_date # If the user wants to search a particular date but # not a particular time, set the end of the range # to 11:59pm on the same date as the start. else: end_date = start_date.replace(hour=23, minute=59) # end if # If searching a range, get the end date from the user. else: go, date = _get_date(wl_obj, "end", start=start_date) # If the user decided to go back here, loop back. if go == GO_BACK: continue # end if # If the user decided to abort, confirm and then # return. if go == QUIT and io_utils.confirm("abort the search"): return None # end if # Now get a time, if the user wants one. go, date = _get_time(wl_obj, "end", date) # If the user decided to go back here, loop back. if go == GO_BACK: continue # end if # If the user decided to abort, confirm and then # return. if go == QUIT and io_utils.confirm("abort the search"): return None # end if # If the user declined to enter a time, set the time # of the datetime object to the end of the day. if type(date) == datetime.date: end_date = datetime.datetime.combine( date, datetime.time.max) # Otherwise just set the datetime object. else: end_date = date # end if # end if # Return the entries to match the search terms. return_list = _find_entries_date(wl_obj, start_date, end_date) # If nothing was found, tell the user. if len(return_list) == 0: io_utils.print_status("Status", "No matches found.", line_length=wl_obj.line_length) # end if return return_list # To view all dates, create a list of unique dates. else: return_list = [] # Iterate through the sorted list. for entry in wl_obj.sorts[DATE_SORT]: # Convert datetime to date and append all unique # dates. if entry[SORT_KEY].date() not in return_list: return_list.append(entry[SORT_KEY].date()) # end if # end for return return_list # end if # end while except Exception as err: _z_exc("wl_search.py/search_by_date", err)
def browse_entries(wl_obj, entry_list, ndx=0): """ Allows the user to browse entries. Arguments: - wl_obj -- the work log object. - entry_list -- the list of entries to browse. Keyword Arguments: - ndx -- the index of the entry to initially display. Returns: the entry list as modified. ----------------------------------------------------------------- """ try: # Loop. while True: # If the list is empty, automatically return. if len(entry_list) == 0: return entry_list # end if # Clear the screen. wl_resource.print_header(wl_obj) # Print status message. msg = f"Displaying task {(ndx + 1)} of {len(entry_list)}" io_utils.print_status("Status", msg, go=True, line_length=wl_obj.line_length) # Display the current entry. display_entry(wl_obj, entry_list[ndx]) # Beneath the entry, display a menu. options = ["Edit", "Delete"] key_list = ["E", "D"] if ndx > 0: options.append("Previous") key_list.append("P") # end if if ndx < (len(entry_list) - 1): options.append("Next") key_list.append("N") # end if options.append("Back") key_list.append("B") response = io_utils.menu(options, keystroke=True, keystroke_list=key_list, lines=False, quit_=False, prompt=" ", line_length=wl_obj.line_length) # Convert the integer response back into the correct option. response = key_list[response - 1] # Take the appropriate action. if response == "B": # If the user wants to go back, return the entry list # (as it may have been modified). return entry_list elif response == "P": # If the user wants to back up one entry, decrement the # index and loop. ndx -= 1 continue elif response == "N": # If the user wants to go to the next entry, increment # the index and loop. ndx += 1 continue elif response == "E": # If the user wants to edit the current entry, call the # edit function and then loop. _edit_entry(wl_obj, entry_list[ndx], ndx, len(entry_list)) continue else: # If the user wants to delete the entry, confirm, and if # confirmed, call the delete function. and then loop. if io_utils.confirm("delete this entry"): _delete_entry(wl_obj, entry_list, ndx) # Delete the entry from the entry list. # If there are no more entries, return the empty # list. if len(entry_list) == 0: return entry_list # end if # If the index is past the end of the list, reset # it. if ndx <= len(entry_list): ndx = len(entry_list) - 1 # end if # end if continue # end if # end while except Exception as err: _z_exc("wl_viewedit.py/browse_entries", err)
def _edit_entry(wl_obj, edit_entry, ndx, total): """ Allows the user to edit a log entry. Arguments: - wl_obj -- the work log object. - edit_entry -- the entry to edit. - ndx -- the number of the entry being edited. - total -- the total number of entries being displayed. Returns: nothing. ----------------------------------------------------------------- """ try: # This function mainly piggybacks on the add functions to alter # the attributes of the log entry object. All values are # preserved via the info attribute of a working copy until # saved by the user. changed = False # Create a working copy of the entry to be edited. new_entry = _copy_entry(edit_entry) # Store the original values in the info attribute. new_entry.info["title"] = new_entry.title new_entry.info["date"] = new_entry.date new_entry.info["time"] = new_entry.time new_entry.info["duration"] = new_entry.duration new_entry.info["notes"] = new_entry.notes new_entry.info["ndx"] = f"task {ndx + 1} of {total}" resort = False # Loop. while True: # Clear the screen and display program header. wl_resource.print_header(wl_obj) # Print status message. io_utils.print_status("Status", f"Editing {new_entry.info['ndx']}…", go=True, line_length=wl_obj.line_length) # Display the entry. display_entry(wl_obj, new_entry, edit=True) # Print instructions. wl_obj.help.print_help(wl_obj.show_help, "Editing", "_eh_edit", line_length=wl_obj.line_length) options = ["Title", "Date", "Time", "Duration", "Notes"] # User selects the field to edit. response = io_utils.menu( options, keystroke_list="#", prompt="Please select a field to edit. When you are finished," + " go back to save or discard your changes:", line_length=wl_obj.line_length, help_toggle=True) # If the user chose to toggle help, do that and loop back. if str(response).lower() == "-h": wl_obj.show_help = not wl_obj.show_help continue # end if # If the user chose to quit... if response == QUIT: # If the entry has been edited, prompt to save changes. if (changed and io_utils.yes_no( "Do you want to save your changes?", line_length=wl_obj.line_length)): # Recalculate the datetime attribute, in case either # the date or time changed. new_entry.datetime = wl_add.add_datetime(new_entry) # Save the changed values to the original log entry # object. _update_entry(wl_obj, new_entry, resort) # Set the flag that the log object has changed. wl_obj.changed = True # end if return # Edit title. elif response == TITLE: ch = wl_add.add_title(wl_obj, new_entry, edit=True) # If the title was edited, turn on the resort flag. if ch: resort = True # end if # Edit date. elif response == DATE: ch = wl_add.add_date(wl_obj, new_entry, edit=True) # If the date was edited, turn on the resort flag. if ch: resort = True # end if # Edit time. elif response == TIME: ch = wl_add.add_time(wl_obj, new_entry, edit=True) # If the time was edited, turn on the resort flag. if ch: resort = True # end if # Edit duration. elif response == DURATION: ch = wl_add.add_duration(wl_obj, new_entry, edit=True) # Edit notes. else: ch = wl_add.add_note(wl_obj, new_entry, edit=True) # end if # If something was edited, turn on the changed flag. if ch: changed = True # end if # end while except Exception as err: _z_exc("wl_viewedit.py/_edit_entry", err)
def calc_duration_abs(wl_obj, string): """ Parses a string and determines the duration it describes. Arguments: - wl_obj -- the work log object. - string -- the string to parse. Returns: a timedelta object if a valid duration is found; else None. ----------------------------------------------------------------- """ try: # If the string is "max", return the maximum timedelta. if string.lower() == "max": return datetime.timedelta.max # end if # Otherwise the string must contain one or more number/word # pairs: a number (or number phrase) and a unit of measure. # These should (but need not be) in descending order. # # First, convert any numbers. raw_list = wl_resource.numbers(string) # Then cull unneeded elements from the list. word_list = [] for word in raw_list: if not ((word is None) or (str(word).lower() == "and")): word_list.append(word) # end if # end for # Now move through the string from left to right. minutes = None hours = None days = None ndx = 0 while ndx < len(word_list): amt = None # An inner loop adds numbers together. If there aren't any, # amt will remain None. (The function must differentiate # here between None and 0.) while ( (ndx < len(word_list) and type(word_list[ndx]) in [int, float])): if amt is None: amt = word_list[ndx] else: amt += word_list[ndx] # end if ndx += 1 # end while # If there was no amount, check for an unspaced number/unit # combination. if amt is None: if re.match(r"\d+m[inutes]?", word_list[ndx]): minutes = int(re.match(r"\d+", word_list[ndx]).group()) ndx += 1 continue if re.match(r"\d+h[ours]?", word_list[ndx]): hours = int(re.match(r"\d+", word_list[ndx]).group()) ndx += 1 continue if re.match(r"\d+d[ays]?", word_list[ndx]): days = int(re.match(r"\d+", word_list[ndx]).group()) ndx += 1 continue # Otherwise just move to the next word. ndx += 1 continue # end if # Determine the units (if not valid, just move to the next # word). But don't do this if the previous word was the # last. if ndx < len(word_list): if re.match(r"m\w*", word_list[ndx]): minutes = amt elif re.match(r"h\w*", word_list[ndx]): hours = amt elif re.match(r"d\w*?", word_list[ndx]): days = amt # end if ndx += 1 # end if # end while # Having gone through the list, see if any times were found. If # not, return None. if (minutes is None) and (hours is None) and (days is None): return None # end if # Change non-present units to zeroes. if not minutes: minutes = 0 # end if if not hours: hours = 0 # end if if not days: days = 0 # end if # Create a timedelta created from the times found. Note that # zero minutes is a valid duration, but negative durations are # not valid. td = datetime.timedelta(days=days, hours=hours, minutes=minutes) # Check if the timedelta is negative. If it is, print an error # and return None. if td < datetime.timedelta(): io_utils.print_status( "Error", "The duration cannot be negative.", line_length=wl_obj.line_length) return None # end if return td except Exception as err: _z_exc("wl_datetime/calc_duration_abs", err)
def _get_duration(wl_obj, d_type, start=None): """ Gets a duration from the user. Arguments: - wl_obj -- the work log object. - d_type -- the type of duration to get (single, min, max) Keyword Arguments: - start -- the min duration (for error checking). Returns: 1) an integer representing the return state: -1 if the user chose to go back, 0 if the user chose to abort completely, 1 if the user successfully entered a duration; 2) if the user enters a valid duration, a timedelta object representing that duration; else None. ----------------------------------------------------------------- """ try: # Build prompt. if d_type == "single": p = "" else: p = f"{d_type} " # end if prompt = f"Enter the {p}duration to search for" if start: prompt += ( ". To search for ALL durations of " + f"{wl_resource.format_string(wl_obj, start)} or greater, " + "enter [max]:") else: prompt += ":" # Loop until a valid response is obtained. while True: # Get a duration. response = io_utils.get_input(prompt) if response == "-b": return -1, None elif response == "-q": return 0, None # end if # Parse the input for a duration. duration = wl_datetime.calc_duration_abs(wl_obj, response) # If no valid duration was entered, print error and prompt # to try again. if duration is None: msg = ( f"{response} could not be interpreted as a valid duration." ) io_utils.print_status("Error", msg, line_length=wl_obj.line_length) # Take a "no" as equivalent of the user aborting. if not io_utils.yes_no("Try again?", line_length=wl_obj.line_length): return 0, None # If "yes", loop back. else: continue # end if # end if return 1, duration # end while except Exception as err: _z_exc("wl_search.py/_get_duration", err)