Last active
July 26, 2018 06:55
-
-
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).
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
""" | |
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', ' ') # 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