This repository has been archived by the owner on Jul 19, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
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
pawel
committed
Sep 23, 2014
1 parent
ddbca06
commit e8616b4
Showing
7 changed files
with
306 additions
and
0 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 |
---|---|---|
@@ -1,2 +1,32 @@ | ||
economic-py | ||
=========== | ||
|
||
# About | ||
These few files were created to help automatically pre fill Economic. | ||
Entries are based on Google calendar events and JIRA tasks currently assigned | ||
to user and matching configurable filter. Duplicates are checked by | ||
looking at task description (Economic) so it's safe to run this command | ||
as many times a day as needed. | ||
|
||
# Installation | ||
Make sure you have `pip` installed on your system by calling: | ||
`sudo apt-get install -y python-pip` | ||
|
||
Once `pip` is installed run command below to install all required python libraries: | ||
`sudo pip install -r requirements.txt` | ||
|
||
After that `config.ini.dist` to `config.ini` and update it with all required | ||
credentials for JIRA, Economic and Google account. | ||
|
||
# Usage | ||
Calling `python run.py` will create all economic entries for today. | ||
What's left it to update hours spent on each task in economic. | ||
|
||
Due to application's limitations (see below) it advised to add new entry | ||
in crontab to make sure all tasks will be registered: | ||
`1 8-17 * * 1-5 root python /path/to/run.py >/dev/null 2>&1` | ||
|
||
# Known limitations | ||
* adding JIRA tasks for day other than current is not supported (might be tricky), | ||
* adding Google calendar events for day other than today is not supported (easy to implement) | ||
* time reported in JIRA is not exported to economic yet |
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,28 @@ | ||
[Jira] | ||
username= | ||
password= | ||
; Name of field in API that holds Economic project ID | ||
economic_field= | ||
; Feel free to improve search query to match your needs. | ||
search_query=assignee=currentUser() and status in ("In Progress") and Sprint in openSprints() | ||
; API endpoint, eg. https://jira.example.com/rest/api/2/ | ||
api_url= | ||
|
||
[Economic] | ||
; Agreement number, should be same for all employees | ||
agreement= | ||
username= | ||
password= | ||
; Default project. Will be used for Google Calendar events | ||
default_project_id= | ||
; Default description. Will be used for all JIRA tasks | ||
default_description= | ||
; Default activity. Will be used for all JIRA tasks | ||
default_activity_id= | ||
|
||
[Google] | ||
;Provided username should end with domain name, eg. @example.com | ||
username= | ||
password= | ||
;List of event names to ignore. | ||
ignore_events=[] |
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,128 @@ | ||
import requests | ||
import re | ||
from datetime import datetime | ||
|
||
|
||
class Economic: | ||
def __init__(self, config): | ||
self.session = requests.session() | ||
self.tasks_html = "" | ||
self.medarbid = "" | ||
|
||
self.config = {} | ||
for item in config: | ||
self.config[item[0]] = item[1] | ||
|
||
self.login() | ||
|
||
def login(self): | ||
response = self.session.post('https://secure.e-conomic.com/secure/internal/login.asp', | ||
{ | ||
'aftalenr': self.config['agreement'], | ||
'brugernavn': self.config['username'], | ||
'password': self.config['password'], | ||
}, | ||
allow_redirects=True) | ||
if 'login.e-conomic.com' in response.content: | ||
raise Exception("ERROR: login to economic failed (check credentials)") | ||
|
||
self.init_medarbid() | ||
self.init_tasks() | ||
|
||
def add_time_entry(self, entry): | ||
""" | ||
Method used to save given dict entry as Economic entry. | ||
Result is based on html response. | ||
@return boolean | ||
""" | ||
if entry['task_description'][:20] in self.tasks_html: | ||
print("SKIPPED - %s" % (entry['task_description'])) | ||
return False | ||
|
||
url = "https://secure.e-conomic.com/secure/applet/df_doform.asp?form=80&medarbid={MEDARBID}&theaction=post" | ||
url = url.replace('{MEDARBID}', self.medarbid) | ||
response = self.session.post(url, | ||
{ | ||
'cs1': str(entry['date']), | ||
'cs2': str(entry['project_id']), | ||
'cs3': str(entry['activity_id']), | ||
'cs6': str(entry['task_description']), | ||
'cs7': str(entry['time_spent']), | ||
'cs10': "False", | ||
'cs11': "False", | ||
'cs4': None | ||
}) | ||
if response.content.find("../generelt/dataedit.asp"): | ||
print("OK - time entry added: %s" % (entry['task_description'])) | ||
return True | ||
|
||
print("ERROR - time entry not added. Entry: %s; Response: %s" % (entry['task_description'], response.content)) | ||
return False | ||
|
||
def convert_calendar_event_to_entry(self, event): | ||
""" | ||
Converts Google Calendar event object to a dict object that will later be inserted to Economic. | ||
""" | ||
try: | ||
start_date = datetime.strptime(event['start_date'][:-10], "%Y-%m-%dT%H:%M:%S") | ||
end_date = datetime.strptime(event['end_date'][:-10], "%Y-%m-%dT%H:%M:%S") | ||
except ValueError: | ||
return None | ||
|
||
time_spent = (end_date - start_date).total_seconds() / 3600 | ||
|
||
activity_id = 4 | ||
if event['title'] == 'Project meeting - Scrum meeting': | ||
activity_id = 2 | ||
|
||
entry = { | ||
'date': str(start_date.isoformat()[:-9]), | ||
'project_id': self.config['default_project_id'], | ||
'activity_id': str(activity_id), | ||
'task_description': event['title'], | ||
'time_spent': str(time_spent).replace('.', ',') | ||
} | ||
|
||
return entry | ||
|
||
|
||
|
||
def convert_jira_task_to_entry(self, task): | ||
""" | ||
Converts JIRA task by adding default activity and description | ||
""" | ||
task['activity_id'] = self.config['default_activity_id'] | ||
task['task_description'] += ' - ' + self.config['default_description'] | ||
task['task_description'] = task['task_description'].decode().encode('utf-8') | ||
|
||
return task | ||
|
||
def init_tasks(self): | ||
""" | ||
This method fetches list of currently entered tasks and saves it as HTML (without parsing). | ||
It will be later used to avoid entering duplicated entries. | ||
""" | ||
url = 'https://secure.e-conomic.com/Secure/generelt/dataedit.asp?' \ | ||
'form=80&projektleder=&medarbid=' + self.medarbid + '&mode=dag&dato=' | ||
today = datetime.now() | ||
date = "%s-%s-%s" % (today.day, today.month, today.year) | ||
response = self.session.get(url + date) | ||
self.tasks_html = response.content | ||
|
||
def init_medarbid(self): | ||
""" | ||
In order to make Economic work we need user ID. Even though we pass User ID in login form, | ||
Economic is internally using another user ID called "medarbid" which indicates specific | ||
user within given agreement number. | ||
Fun fact: changing this ID allows you to add entries for another user without need for his credentials. | ||
""" | ||
url = "https://secure.e-conomic.com/Secure/subnav.asp?subnum=10" | ||
response = self.session.get(url) | ||
|
||
medarbid = re.search(r'medarbid=(\d+)', response.content) | ||
if medarbid: | ||
self.medarbid = medarbid.groups()[0] | ||
else: | ||
raise RuntimeError('There is problem when trying to determine economic internal user id.') |
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,29 @@ | ||
import gdata.calendar.client | ||
import gdata.client | ||
|
||
|
||
class Calendar: | ||
def __init__(self, username, password, ignore_events): | ||
try: | ||
self.username = username[:username.find('@') + 1] | ||
self.cal_client = gdata.calendar.client.CalendarClient(source='Google-Calendar_Python_Sample-1.0') | ||
self.cal_client.ClientLogin(username, password, self.cal_client.source) | ||
self.ignore_events = ignore_events | ||
except gdata.client.BadAuthentication: | ||
raise Exception('ERROR: login to Google failed (check credentials)') | ||
|
||
def get_events(self, start_date, end_date): | ||
""" | ||
Get events from calendar between given dates. | ||
""" | ||
query = gdata.calendar.client.CalendarEventQuery(start_min=start_date, start_max=end_date, singleevents="true") | ||
feed = self.cal_client.GetCalendarEventFeed(q=query) | ||
for i, an_event in zip(range(len(feed.entry)), feed.entry): | ||
for who in [x for x in an_event.who if self.username in x.email]: | ||
if who.attendee_status is not None and "declined" not in who.attendee_status.value: | ||
for an_when in [x for x in an_event.when if an_event.title.text not in self.ignore_events]: | ||
yield { | ||
'start_date': an_when.start, | ||
'end_date': an_when.end, | ||
'title': an_event.title.text | ||
} |
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,47 @@ | ||
import json | ||
import requests | ||
import datetime | ||
|
||
|
||
class Jira: | ||
def __init__(self, config): | ||
self.config = {} | ||
for item in config: | ||
self.config[item[0]] = item[1] | ||
|
||
self.auth_data = (self.config['username'], self.config['password']) | ||
|
||
def make_request(self, uri): | ||
""" | ||
Generic method for making requests to JIRA API. | ||
""" | ||
response = requests.get(self.config['api_url'] + uri, auth=self.auth_data) | ||
try: | ||
if response.status_code != 200: | ||
raise Exception('ERROR: login to JIRA failed (check credentials)') | ||
|
||
return json.loads(response.text) | ||
except ValueError: | ||
raise Exception("ERROR - Couldn't fetch JIRA tasks.") | ||
|
||
def get_tasks(self): | ||
""" | ||
Generator returning all tasks assigned to current user that match filter specified in configuration | ||
""" | ||
tasks = self.make_request( | ||
'search?jql=' + self.config['search_query'] + '&fields=*all,-comment' | ||
) | ||
if not tasks: | ||
return | ||
|
||
for issue in tasks['issues']: | ||
try: | ||
task = {'date': datetime.datetime.now().isoformat()[:10], | ||
'project_id': str(issue['fields'][self.config['economic_field']]['value'].split('-')[0]), | ||
'task_description': '%s %s' % (issue['key'], issue['fields']['summary']), 'time_spent': '0'} | ||
yield task | ||
# Possible issue: no economic project set. | ||
except KeyError: | ||
if self.config['economic_field'] not in issue['fields']: | ||
print('ERROR - task %s is missing economic project ID' % (issue['key'])) | ||
yield None |
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,2 @@ | ||
gdata==2.0.18 | ||
requests==2.2.1 |
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,42 @@ | ||
#!/usr/bin/env python | ||
import datetime | ||
import ConfigParser | ||
import os | ||
from gcal import Calendar | ||
from jira import Jira | ||
from economic import Economic | ||
|
||
try: | ||
configFile = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'config.ini') | ||
if not os.path.isfile(configFile): | ||
raise Exception('Configuration file config.ini not found.') | ||
config = ConfigParser.ConfigParser() | ||
config.read(configFile) | ||
|
||
economic = Economic(config.items('Economic')) | ||
|
||
# Add entries from Google Calendar. | ||
try: | ||
calendar = Calendar(config.get('Google', 'username'), config.get('Google', 'password'), | ||
config.get('Google', 'ignore_events')) | ||
today = datetime.datetime.now().isoformat()[:10] | ||
tomorrow = (datetime.datetime.now() + datetime.timedelta(days=1)).isoformat()[:10] | ||
for event in calendar.get_events(today, tomorrow): | ||
entry = economic.convert_calendar_event_to_entry(event) | ||
if entry: | ||
economic.add_time_entry(entry) | ||
except Exception as e: | ||
print(e.message) | ||
|
||
# Add entries from JIRA. | ||
try: | ||
jira = Jira(config.items('Jira')) | ||
for task in jira.get_tasks(): | ||
if task: | ||
task = economic.convert_jira_task_to_entry(task) | ||
economic.add_time_entry(task) | ||
except Exception as e: | ||
print(e.message) | ||
|
||
except Exception as e: | ||
print(e.message) |