Monday, June 04, 2007

From Basecamp to MS Project

I don't hide the fact that I really like Basecamp from anyone I know. Free for single projects and with a relatively low price-point, I've gotten one of my clients to use it for basic tracking of projects. I've also logged into Huddle ( a similar DotNet offering from British-based Ninian Solutions)) but haven't gotten right into it just yet.



One of the great things offered by Basecamp is the Basecamp API - which they describe as plain vanilla xml over HTTP.  A lot of the samples that integrate with Basecamp are primarily web-based or widgets. I think that's primarily because of the synergy of the Web 2.0 world - but you can just as easily integrate Basecamp with Windows-based apps directly with a little bit of COM automation.



While I did some basic stuff like posting messages and updating items with the API, I found imified more useful to do updates over instant messenger instead.



However, when one of our sales managers asked for an MS Project file of all the outstanding Basecamp projects, I knew it was a challenge I wanted to face.(in FoxPro and West-Wind IP Tools - of course!)



I created a number of classes to accomplish this, using collections and custom classes.



PUBLIC loBase

loBase = CREATEOBJECT("Custom")

loBase.AddObject("Projects","Collection")

loBase.AddObject("Users","Collection")

loBase.AddObject("Companies","Collection")



But the basis for everything else was pretty straight forward:

lox = lo.gethttp()

? lox.httpconnect(basecampsite,"username","password",.t.)

 lox.nhttppostmode = 4

LOCAL loXML

lcPosts = " "

lnText = 0

lnResult=lox.HTTPGetEx("/project/list",@lcPosts,@lnText,"Accept: application/xml")



loXML = CREATEOBJECT("MSXML.DomDocument")

IF loXML.LoadXML(lcPosts)

    loProjects = loXML.selectnodes("//project")

    FOR lni = 1 TO loProjects.length

        loProj =loProjects.item(lni-1)

        WITH loBase.Projects

        .Add(NEWOBJECT("cbaseproject"))

        .Item(.Count).ProjectName =loProj.selectsinglenode("name").nodetypedvalue

        .Item(.Count).ProjectID =loProj.selectsinglenode("id").nodetypedvalue

        .Item(.Count).Status =loProj.selectsinglenode("status").nodetypedvalue

        ENDWITH

    ENDFOR

ENDIF



I did a similar part for Companies and Users in the system. This was necessary to link projects to individuals and the like.



Within the cbaseproject class, I created a method called GetTasks , GetPeople and GetMilestones that all looked pretty much like this:

DEFINE CLASS cBaseProject AS Custom

    ProjectID = 0

    ProjectName = ""

    Status = ""

    Company = ""

   

    PROCEDURE GetTasks

    lcPosts = " "

    lnText = 0

   

** Reconnect


lnResult=lox.HTTPGetEx("/projects/"+THIS.ProjectID+"/todos/lists",@lcPosts,@lnText,"Accept: application/xml")

loXML = CREATEOBJECT("MSXML.DomDocument")

IF loXML.LoadXML(lcPosts)

    loProjects = loXML.selectnodes("//todo-list")

    THIS.AddObject("TaskLists","Collection")

    FOR lni = 1 TO loProjects.length

        loProj =loProjects.item(lni-1)

        WITH THIS.TaskLists

            .Add(NEWOBJECT("cbasetask"))

            .Item(.Count).TaskName =loProj.selectsinglenode("name").nodetypedvalue

            .Item(.Count).Description =loProj.selectsinglenode("description").nodetypedvalue

            .Item(.Count).ID =loProj.selectsinglenode("id").nodetypedvalue

        ENDWITH

    ENDFOR

ENDIF



    ENDPROC

   

    PROCEDURE GetMilestones

    lcPosts = " "

    lnText = 0

   

** Reconnect

lnResult=lox.HTTPGetEx("/projects/"+THIS.ProjectID+"/milestones/list",@lcPosts,@lnText,"Accept: application/xml")

loXML = CREATEOBJECT("MSXML.DomDocument")

IF loXML.LoadXML(lcPosts)

    loProjects = loXML.selectnodes("//milestone")

    THIS.AddObject("MileStones","Collection")

    FOR lni = 1 TO loProjects.length

        loProj =loProjects.item(lni-1)

        WITH THIS.Milestones

            .Add(NEWOBJECT("cMilestone"))

            .Item(.Count).Title =loProj.selectsinglenode("title").nodetypedvalue

            .Item(.Count).ID =loProj.selectsinglenode("id").nodetypedvalue

            .Item(.Count).Deadline =loProj.selectsinglenode("deadline").nodetypedvalue

            .Item(.Count).xml = loProj.Xml

            .Item(.Count).person = getperson(loProj.selectsinglenode("responsible-party-id").nodetypedvalue)

            TRY

            .Item(.Count).Completed = loProj.selectsinglenode("completed").nodetypedvalue="true"

            .Item(.Count).CompletedOn =loProj.selectsinglenode("completed-on").nodetypedvalue

            CATCH

            ENDTRY

        ENDWITH

    ENDFOR

ENDIF



    ENDPROC



  

ENDDEFINE



From here, I was now simply going to create or update my MS Project file.



LOCAL lcText

loapp = GETOBJECT(,"MSProject.application")

loProject = loapp.activeproject



LOCAL lnStartRow

lnStartRow=0

lcText = ""

LOCAL loP

FOR EACH loP IN lobase.projects

    IF NOT PEMSTATUS(lop,"milestones",5)

        lop.GetMilestones()

    ENDIF

    loProject.Tasks.add(lop.projectname)

    lnStartRow=lnStartRow+1

    loApp.selectrange(lnStartRow,3,.f.)

    loApp.outlineoutdent()

    lnS = 0

    FOR EACH loM IN lop.Milestones

        lnS = lns+1

        tsk = loProject.Tasks.add(lom.Title)

        tsk.milestone = .t.

        lnStartRow=lnStartRow+1

        loApp.selectrange(lnStartRow,3,.f.)

        IF lnS=1

            loApp.outlineindent()

        ENDIF

        IF NOT lom.Completed

            *!* ? lom.Title + " due on " + lom.deadline +" by "+lom.person

            tsk.finish = DATE(VAL(LEFT(lom.deadline,4)),VAL(SUBSTR(lom.deadline,6,2)),VAL(RIGHT(lom.deadline,2)))

        ELSE

            tsk.percentcomplete = 100

            tsk.finish = DATE(VAL(LEFT(lom.completedon,4)),VAL(SUBSTR(lom.completedon,6,2)),VAL(SUBSTR(lom.completedon,9,2)))

        ENDIF

    ENDFOR

ENDFOR





The end result? I can give a project file that always includes the latest updates to those who like the graphics appeal and project-reporting of MS Project.





Basecamp API



Powered by ScribeFire.

No comments: