Skip to content

Instantly share code, notes, and snippets.

@nmz787
Last active July 26, 2018 06:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nmz787/61a8e8d774913a2af90471b478a09451 to your computer and use it in GitHub Desktop.
Save nmz787/61a8e8d774913a2af90471b478a09451 to your computer and use it in GitHub Desktop.
Converts a CSV with columns [Type, Duration, Name, Description] into Gnome Planner format (which can generate a Gantt chart).
"""
usage: csvToGnomePlanner.py <input path> <output path> <start YYYYMMDD>
Converts a CSV with columns [Type, Duration, Name, Description] into Gnome Planner format (which can generate a Gantt chart).
where Column Definitions are:
Type : 'Subtask' or anything else (normal Task is implied)
Duration : '<number> <unit>' where units are [day, week, month]
NOTE: Tasks which have Subtasks CANNOT have duration... they derive this from the Subtasks
Name : text
Description : text
Example Input CSV:
Type,Duration,Name,Description
,1 day,1 day - name 1,Description 1
,2 wk,2 wk - name 2,Description 2
,,name 3 - do this thing composed of subtasks,Description 3
subtask,1 wk,1 wk - name 4,Description 4
subtask,2 wk,2 wk - name 5,Description 5
,1 d,1d - name 6,Description 6
"""
import csv
import datetime
import re
import sys
prefix = '''<?xml version="1.0"?>
<project name="" company="" manager="" phase="" project-start="{}" mrproject-version="2" calendar="1">
<properties/>
<phases/>
<calendars>
<day-types>
<day-type id="0" name="Working" description="A default working day"/>
<day-type id="1" name="Nonworking" description="A default non working day"/>
<day-type id="2" name="Use base" description="Use day from base calendar"/>
</day-types>
<calendar id="1" name="Default">
<default-week mon="0" tue="0" wed="0" thu="0" fri="0" sat="1" sun="1"/>
<overridden-day-types>
<overridden-day-type id="0">
<interval start="0800" end="1200"/>
<interval start="1300" end="1700"/>
</overridden-day-type>
</overridden-day-types>
<days/>
</calendar>
</calendars>
<tasks>'''
suffix = '''</tasks>
<resource-groups/>
<resources/>
<allocations/>
</project>'''
class Task():
def __init__(self, task_id, name, note, work, start, end, work_start, percent_complete, priority, work_type, scheduling, predecessor_id=None, subtasks=False):
self.task_id = task_id
self.name = name
self.note = note
self.work = work
self.start = start
self.end = end
self.work_start = work_start
self.percent_complete = percent_complete
self.priority = priority
self.work_type = work_type
self.scheduling = scheduling
self.predecessor_id = predecessor_id
self.subtasks = subtasks if subtasks else []
def add_subtask(self, subtask):
self.subtasks.append(subtask)
def get_task_line(self):
task_line = '<task id="{}" name="{}" note="{}" work="{}" start="{}" end="{}" work-start="{}" percent-complete="{}" priority="{}" type="{}" scheduling="{}">'
line = task_line.format(self.task_id, self.name, self.note, self.work, self.start, self.end, self.work_start, self.percent_complete, self.priority, self.work_type, self.scheduling)
if self.predecessor_id and not self.subtasks:
pred_line = '\n <predecessors>\n <predecessor id="1" predecessor-id="{}" type="FS"/>\n</predecessors>\n'.format(self.predecessor_id)
line=line+pred_line
return line
def get_task(self):
main = self.get_task_line()
for subtask in self.subtasks:
main = main + ' ' + subtask.get_task_line() +'</task>\n'
main = main + '</task>\n'
return main
def get_planner_formatted_time(t, start_date=None):
#hour = '08' if not start_date else start_date
return t.strftime("%Y%m%dT%H0000Z")
day_counter = 0
def get_time(start_date, num_days):
'''
Planner has the idea of a start time and a work-start time.
The former is often the end of the previous task, or maybe it could be considered the 'legal date' i.e 12:00 AM
while work-start is the 'start of the work day', i.e. 8 AM
'''
global day_counter
# work-start, 8AM
t1 = (datetime.datetime(*start_date) + datetime.timedelta(days=day_counter) + datetime.timedelta(hours=8))
# task 'start'
if not day_counter:
t2 = datetime.datetime(*start_date)
else:
t2 = (datetime.datetime(*start_date) + datetime.timedelta(days=day_counter-1) + datetime.timedelta(hours=16))
day_counter += num_days
# task 'end' (end of workday, 4PM)
t3 = (datetime.datetime(*start_date) + datetime.timedelta(days=day_counter-1) + datetime.timedelta(hours=16))
# calculate the amount of work, one day == 28800 (I guess 1 unit == 3 seconds)
work = 28800*num_days
return (get_planner_formatted_time(t1), get_planner_formatted_time(t2), get_planner_formatted_time(t3), work)
def calc_days_diff(a, b):
'''if you wanted to calculate the difference between two dates, to get the number of days
'2017-04-24' - '2017-04-26' == 2
'''
y,m,d = a.split('-')
Y,M,D = b.split('-')
y,m,d = int(y),int(m),int(d)
Y,M,D = int(Y),int(M),int(D)
return abs((datetime.date(y,m,d) - datetime.date(Y,M,D)).days)
def parse_duration_to_days(duration):
''' parses inputs like '1 wk' or '1 day' or '1 month' and returns equivalent number of days '''
num, unit = re.match(r'(\d+)[^\w]*(\w+)', duration).groups()
num = int(num)
if unit in ['w', 'wk', 'week', 'wks', 'weeks']:
return 7*num
if unit in ['day', 'days', 'd']:
return num
if unit in ['m', 'month', 'months']:
return 30*num
if __name__=='__main__':
'''Get the input and output filepaths, parse the CSV into lines and then into Task objects.
Then write the output file with the task XML lines.'''
#start_date = (2018, 9, 1)
try:
inp = sys.argv[1]
out = sys.argv[2]
y = int(sys.argv[3][0:4])
m = int(sys.argv[3][4:6])
d = int(sys.argv[3][6:8])
start_date = (y,m,d)
except:
print('usage: <input path> <output path> <start YYYYMMDD>')
sys.exit(-1)
with open(inp) as inpf:
r = csv.reader(inpf)
lines = list(r)
header = {x:i for i,x in enumerate(lines[0])}
tasks = []
project_start_date = get_planner_formatted_time(datetime.datetime(*start_date))
for i, line in enumerate(lines[1:]):
name = line[header['Name']]
desc = line[header['Description']].replace('\n', '&#10;') # handle the way Planner shows newlines
is_subtask = line[header['Type']].lower().endswith('subtask')
duration = line[header['Duration']]
if duration:
num_days = parse_duration_to_days(duration)
work_start, task_start, end, work = get_time(start_date, num_days)
else:
work_start=''
task_start=''
end=''
work=0
# Planner has the idea of a start time and a work-start time.
# The former is often the end of the previous task, or maybe it could be considered the 'legal date' i.e 12:00 AM
# while work-start is the 'start of the work day', i.e. 8 AM
print('{} - {} - {} - {} {}'.format(task_start, work_start, end, duration, name, num_days))
t = Task(i+1, name, desc, work, task_start, end, work_start, 0, 0, 'normal', 'fixed-work', '' if not i else i)
if not is_subtask:
tasks.append(t)
else:
# a Task with Subtasks can't have a predecessor
tasks[-1].predecessor_id = None
# the first Subtask must have it's predecessor adjusted to point to before the parent Task
if not tasks[-1].subtasks:
t.predecessor_id -= 1
tasks[-1].subtasks.append(t)
with open(out, 'w') as outf:
outf.write(prefix.format(project_start_date))
for task in tasks:
outf.write(task.get_task())
outf.write(suffix)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment