Skip to content

Commit

Permalink
Add tools/delete_activities
Browse files Browse the repository at this point in the history
  • Loading branch information
brunetton committed Jan 20, 2015
1 parent 0866dc9 commit 7a04099
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 76 deletions.
97 changes: 97 additions & 0 deletions lib/common.py
Original file line number Diff line number Diff line change
@@ -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['<start>'], config, quit_if_none=True)
if args['to']:
to_date = parse_date_or_days_ahead(args['<stop>'], config)
if args['<date>']:
for_date = parse_date_or_days_ahead(args['<date>'], 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
84 changes: 8 additions & 76 deletions redminetimesync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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['<start>'], config, quit_if_none=True)
if args['to']:
to_date = parse_date_or_days_ahead(args['<stop>'], config)
if args['<date>']:
for_date = parse_date_or_days_ahead(args['<date>'], config, quit_if_none=True)
return args, from_date, to_date, for_date


if __name__ == '__main__':
# Read config file
Expand All @@ -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')
Expand Down Expand Up @@ -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:
Expand Down
130 changes: 130 additions & 0 deletions tools/delete_activities.py
Original file line number Diff line number Diff line change
@@ -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 <start> [(to <stop>)] [options]
{self_name} <date> [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['<date>'] == '0':
question = "Delete tasks for today ?"
elif args['<date>'] == '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

0 comments on commit 7a04099

Please sign in to comment.