Skip to content
This repository has been archived by the owner on Jul 19, 2021. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pawel committed Sep 23, 2014
1 parent ddbca06 commit e8616b4
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 0 deletions.
30 changes: 30 additions & 0 deletions README.md
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
28 changes: 28 additions & 0 deletions config.ini.dist
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=[]
128 changes: 128 additions & 0 deletions economic.py
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.')
29 changes: 29 additions & 0 deletions gcal.py
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
}
47 changes: 47 additions & 0 deletions jira.py
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
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
gdata==2.0.18
requests==2.2.1
42 changes: 42 additions & 0 deletions run.py
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)

0 comments on commit e8616b4

Please sign in to comment.