-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
235 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||