From 7a04099b06acae5eb843d0bb29fd668d36d28c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Duy=C3=A9?= Date: Tue, 20 Jan 2015 17:01:49 +0100 Subject: [PATCH] Add tools/delete_activities --- lib/common.py | 97 +++++++++++++++++++++++++++ redminetimesync.py | 84 +++--------------------- tools/delete_activities.py | 130 +++++++++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 76 deletions(-) create mode 100644 lib/common.py create mode 100755 tools/delete_activities.py diff --git a/lib/common.py b/lib/common.py new file mode 100644 index 0000000..ec8ebe7 --- /dev/null +++ b/lib/common.py @@ -0,0 +1,97 @@ +import datetime +import sys + +import moment # https://pypi.python.org/pypi/moment + + +def print_(string): + '''Print the string without end line break''' + print(string), # Here the end-line coma is intended + sys.stdout.flush() + + +# -*- Parsing args functions -*- + + +def parse_date(datestr, date_formats): + '''Try all dates formats defined in date_formats array and returns a Moment object representing that date. + If format doesn't containt year, default assign current year to returned date (instead of 1900). + Returns: Moment object or None + ''' + assert datestr + assert date_formats + for date_format in date_formats: + date_format = date_format.strip() + try: + date = moment.date(datestr, date_format) + if date_format.find('Y') == -1: + # date format doesn't containts year + current_year = datetime.date.today().year + return date.replace(year=current_year) + else: + return date + except ValueError: + pass + return None + + +def quit_with_parse_date_error(datestr, date_formats): + print "Error while parsing date '{}'.\nAccepted formats defined in config file are: {}."\ + .format(datestr, date_formats) + sys.exit(-1) + + +def parse_date_or_days_ahead(datestr, config, quit_if_none=False): + '''Returns a moment date corresponding to given date, or days ahead number. + quit_if_none: quit programm if no date parsed + + parse_date_or_days_ahead('4/10/2014') should return corresponding moment, if that format is defined in config file + parse_date_or_days_ahead('1') should return the date of yesterday + ''' + # Try to find a formatted date + date_formats = config.get('default', 'date_formats').split(',') + date = parse_date(datestr, date_formats) + if date: + return date + # It's not a date; maybe is it a number corresponding to some days ago + if datestr.isdigit(): + # It's a number, corresponding to some days ago from today. Retun that date + return moment.now().subtract(days=int(datestr)) + if quit_if_none: + quit_with_parse_date_error(datestr, date_formats) + return None + + +def parse_dates_in_args(args, config): + '''Returns from_date, to_date or for_date Moment dates from given args array + nb: if from_date is not None; to_date could be None or not. + ''' + assert args + from_date = to_date = for_date = None + if args['from']: + from_date = parse_date_or_days_ahead(args[''], config, quit_if_none=True) + if args['to']: + to_date = parse_date_or_days_ahead(args[''], config) + if args['']: + for_date = parse_date_or_days_ahead(args[''], config, quit_if_none=True) + return from_date, to_date, for_date + + +# -*- Config file parsing functions -*- + + +def get_api_key_or_login_password(config): + '''Check that api_key or username (and eventually password) are given in config file + Returns: api_key, login, password + ''' + api_key = login = password = None + if config.has_option('redmine', 'key'): + api_key = config.get('redmine', 'key') + if config.has_option('redmine', 'login'): + login = config.get('redmine', 'login') + if config.has_option('redmine', 'password'): + password = config.get('redmine', 'Password') + if not api_key and not login: + print('No API key nor Redmine login found in config file.\nPlease Edit your config file.') + sys.exit(-1) + return api_key, login, password diff --git a/redminetimesync.py b/redminetimesync.py index c568db8..d63a939 100755 --- a/redminetimesync.py +++ b/redminetimesync.py @@ -23,6 +23,10 @@ ) +sys.path.append('.') +from lib import common +from lib.common import print_ + ACTIVITIES_CONFIG_FILE = 'activities.config' CONFIG_FILE = 'redminetimesync.config' @@ -48,11 +52,6 @@ ''' -def print_(string): - '''Print the string without end line break''' - print(string), # Here the end-line coma is intended - sys.stdout.flush() - def getTimeEntries(date, config): '''Reads Sqlite Redmine DB file and return an array of explicit associative array for times entries, filtering out entries that do not match issue_id_regexp defined in config file @@ -142,6 +141,7 @@ def fetchFromDatabase(db_filename, date): print "\n\nTotal : {}h".format(round(total_duration, 1)) return activities, total_duration + def syncToRedmine(time_entries, date, redmine): '''Push all given time_entries to Redmine''' def issue_exists(id, redmine): @@ -182,66 +182,6 @@ def issue_exists(id, redmine): print "Connection Error: {}".format(e.message) print "\n" -def parse_date(datestr, date_formats): - '''Try all dates formats defined in date_formats array and returns a Moment object representing that date. - If format doesn't containt year, default assign current year to returned date (instead of 1900). - Returns: Moment object or None - ''' - assert datestr - assert date_formats - for date_format in date_formats: - date_format = date_format.strip() - try: - date = moment.date(datestr, date_format) - if date_format.find('Y') == -1: - # date format doesn't containts year - current_year = datetime.date.today().year - return date.replace(year=current_year) - else: - return date - except ValueError: - pass - return None - - -def parse_command_line_args(): - '''Parse command line args and returns args, from_date, to_date or for_date Moment dates. - nb: if from_date is not None; to_date could be None or not. - ''' - def quit_with_parse_date_error(datestr, date_formats): - print "Error while parsing date '{}'.\nAccepted formats defined in config file are: {}."\ - .format(datestr, date_formats) - sys.exit(-1) - def parse_date_or_days_ahead(datestr, config, quit_if_none=False): - '''Returns a moment date corresponding to given date, or days ahead number. - quit_if_none: quit programm if no date parsed - - parse_date_or_days_ahead('4/10/2014') should return corresponding moment, if that format is defined in config file - parse_date_or_days_ahead('1') should return the date of yesterday - ''' - # Try to find a formatted date - date_formats = config.get('default', 'date_formats').split(',') - date = parse_date(datestr, date_formats) - if date: - return date - # It's not a date; maybe is it a number corresponding to some days ago - if datestr.isdigit(): - # It's a number, corresponding to some days ago from today. Retun that date - return moment.now().subtract(days=int(datestr)) - if quit_if_none: - quit_with_parse_date_error(datestr, date_formats) - return None - - args = docopt(DOC.format(self_name=os.path.basename(__file__))) - from_date = to_date = for_date = None - if args['from']: - from_date = parse_date_or_days_ahead(args[''], config, quit_if_none=True) - if args['to']: - to_date = parse_date_or_days_ahead(args[''], config) - if args['']: - for_date = parse_date_or_days_ahead(args[''], config, quit_if_none=True) - return args, from_date, to_date, for_date - if __name__ == '__main__': # Read config file @@ -253,7 +193,8 @@ def parse_date_or_days_ahead(datestr, config, quit_if_none=False): config.read(CONFIG_FILE) # Parse command line parameters - args, from_date, to_date, for_date = parse_command_line_args() + args = docopt(DOC.format(self_name=os.path.basename(__file__))) + from_date, to_date, for_date = common.parse_dates_in_args(args, config) # Get prefered date format from config file to display dates date_format = config.get('default', 'date_formats') @@ -292,16 +233,7 @@ def parse_date_or_days_ahead(datestr, config, quit_if_none=False): sys.exit() # Check that api_key or username (and eventually password) are given in config file - api_key = login = password = None - if config.has_option('redmine', 'key'): - api_key = config.get('redmine', 'key') - if config.has_option('redmine', 'login'): - login = config.get('redmine', 'login') - if config.has_option('redmine', 'password'): - password = config.get('redmine', 'Password') - if not api_key and not login: - print('No API key nor Redmine login found in config file.\nPlease Edit your config file.') - sys.exit(-1) + api_key, login, password = common.get_api_key_or_login_password(config) # Connects to Redmine if api_key: diff --git a/tools/delete_activities.py b/tools/delete_activities.py new file mode 100755 index 0000000..e7e2d02 --- /dev/null +++ b/tools/delete_activities.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from ConfigParser import RawConfigParser +import os +from requests import ConnectionError +import sys + +from docopt import docopt # http://docopt.org/ +import moment # https://pypi.python.org/pypi/moment +from redmine import Redmine # https://pypi.python.org/pypi/python-redmine +from redmine.exceptions import AuthError + +sys.path.append('..') +from lib import common +from lib.common import print_ + + +CONFIG_FILE = '../redminetimesync.config' + +DOC = ''' +Tool for mass activities delete + +Usage: + {self_name} from [(to )] [options] + {self_name} [options] + {self_name} -h | --help + +Options: + -u --user [user_id] Restrict deletions to user_id + -a --auto Do not ask for manual validation for each day, sync all days in given interval +''' + + +if __name__ == '__main__': + # Read config file + if not os.path.isfile(CONFIG_FILE): + print('Can\'t find config file: {}\nYou can copy template conf file and adapt.'.format(CONFIG_FILE)) + sys.exit(-1) + + config = RawConfigParser() + config.read(CONFIG_FILE) + + # Parse command line parameters + args = docopt(DOC.format(self_name=os.path.basename(__file__))) + from_date, to_date, for_date = common.parse_dates_in_args(args, config) + user_id = None + if args['--user']: + assert args['--user'].isdigit() + user_id = args['--user'] + else: + print 'WARNING: you didn\'t specified an user id; deleting tasks for ALL users in Redmine !\n' + + # Get prefered date format from config file to display dates + date_format = config.get('default', 'date_formats') + if date_format.find(',') != -1: + # More than one format is defined, take first + date_format = (date_format.split(',')[0]).strip() + + # print confirmation to user, to check dates + if from_date: + if to_date is None: + # implicitly takes today for to_date + to_date = moment.now() + question = "Delete tasks from {} to today (included) ?".format(from_date.format(date_format)) + else: + question = "Delete tasks from {} to {} (included) ?".format( + from_date.format(date_format), + to_date.format(date_format) + ) + elif for_date: + if args[''] == '0': + question = "Delete tasks for today ?" + elif args[''] == '1': + question = "Delete tasks for yesterday ({}) ?".format(for_date.format(date_format)) + else: + question = "Delete tasks for {} ?".format(for_date.format(date_format)) + assert question + + print question + print_("\nPress ENTER to validate ...") + try: + raw_input('') + print "\n" + except KeyboardInterrupt: + print "\n" + sys.exit() + + # Check that api_key or username (and eventually password) are given in config file + api_key, login, password = common.get_api_key_or_login_password(config) + + # Connects to Redmine + if api_key: + redmine = Redmine(config.get('redmine', 'url'), key=api_key) + else: + if not password: + password = getpass.getpass('{}\'s password: '.format(login)) + redmine = Redmine(config.get('redmine', 'url'), username=login, password=password) + print_('-> Connecting to Redmine...') + + try: + redmine.auth() + except (AuthError, ConnectionError) as e: + print "\nConnection error: {}".format(e.message) + sys.exit(-1) + print_(' OK') + print "\n" + + if for_date: + # only one date will be parsed + from_date = for_date + to_date = for_date + + # Get time entries from Redmine + time_entries = redmine.time_entry.filter(user_id=user_id, from_date=from_date.date, to_date=to_date.date) + if len(time_entries) == 0: + print "-> No times entries found." + sys.exit() + + for t in time_entries: + print "{} {} #{} {}h {} {}".format(t.spent_on, t.user, t.issue, t.hours, t.activity, t.project) + if not args['--auto']: + print_("Press ENTER to delete this entry ...") + try: + raw_input('') + except KeyboardInterrupt: + print "\n" + sys.exit() + redmine.time_entry.delete(t) + print