Verbose Logging

software development with some really amazing hair

T + G I F R

Basecamp Next Todo Migrator

· · Posted in Programming
Tagged with

I had this conversation on Twitter yesterday.

Twitter conversation with @37Signals about migrating a BCX project

That's fine. 37Signals is known for saying no to things in the interest of a better product. A migrator for BCX isn't exactly something that would add enough value to the product to warrant spending their time on it.

It's cool.

Don't worry, I got this

I built one. It only took a couple hours to throw together and clean up. It's super basic, but it did the job for me. It handles todolists with their todos and comments, but no attachments.

require 'httparty'
require 'active_support/all'
module PartyPooper
def get(url, options = {})
self.class.get("/#{account_id}/api/v1/#{url}", options.merge(extras))
rescue => boom
puts boom.message
retry
end
def post(url, data, options = {})
self.class.post("/#{account_id}/api/v1/#{url}", options.merge(extras(post_headers)).merge(body: data.to_json))
rescue => boom
puts boom.message
retry
end
def put(url, data, options = {})
self.class.put("/#{account_id}/api/v1/#{url}", options.merge(extras(post_headers)).merge(body: data.to_json))
rescue => boom
puts boom.message
retry
end
def extras(h = headers)
{ basic_auth: basic_auth, headers: h }
end
def basic_auth
{ username: username, password: password }
end
def headers
{ 'User-Agent' => user_agent }
end
def post_headers
headers.merge('Content-Type' => 'application/json')
end
end
Client = Struct.new(:username, :password, :user_agent, :account_id, :project_id) do
include HTTParty
include PartyPooper
base_uri 'https://basecamp.com'
def todo_lists
get("/projects/#{project_id}/todolists.json")
end
def completed_todo_lists
get("/projects/#{project_id}/todolists/completed.json")
end
def todo_list(id)
get("/projects/#{project_id}/todolists/#{id}.json")
end
def create_todo_list(data)
post("/projects/#{project_id}/todolists.json", data)
end
def todos(list)
get("/projects/#{project_id}/todolists/#{list}.json")
end
def todo(id)
get("/projects/#{project_id}/todos/#{id}.json")
end
def create_todo(list, data)
post("/projects/#{project_id}/todolists/#{list}/todos.json", data)
end
def complete_todo(id)
put("/projects/#{project_id}/todos/#{id}.json", completed: true)
end
def create_todo_comment(id, data)
post("/projects/#{project_id}/todos/#{id}/comments.json", data)
end
end
Migrator = Struct.new(:from, :to, :log) do
def migrate
lists.each { |list| migrate_list(list) }
end
def migrate_list(list)
log.info("Migrating #{list['name']}")
destination = to.create_todo_list(list.slice('name'))
todos_from_list(list).each { |todo| migrate_todo(todo, destination) }
end
def migrate_todo(todo, destination)
log.info("\tMigrating todo #{todo['content']}")
todo = from.todo(todo['id'])
ctodo = to.create_todo(destination['id'], todo.slice('content'))
comments = comments_from_todo(todo)
if comments.present?
log.info("\tMigrating #{comments.count} comments")
comments.each { |comment| migrate_comment(comment, ctodo) }
end
complete_todo(ctodo) if todo['completed']
end
def complete_todo(todo)
log.info("\tCompleting #{todo['content']}")
to.complete_todo(todo['id'])
end
def comments_from_todo(todo)
Array(todo['comments'])
end
def migrate_comment(comment, todo)
to.create_todo_comment(todo['id'], comment.slice('content'))
end
def lists
from.completed_todo_lists.reverse + from.todo_lists.reverse
end
def todos_from_list(list)
todos = from.todo_list(list['id'])['todos']
Array(todos['remaining']) + Array(todos['completed'])
end
end
view raw migrator.rb hosted with ❤ by GitHub
require 'logger'
require_relative 'client'
username = 'username'
password = 'password'
user_agent = 'BCX Mover (email@example.com)'
from_account_id = 1234
from_project_id = 2345
to_account_id = 5678
to_project_id = 6789
from = Client.new(username, password, user_agent, from_account_id, from_project_id)
to = Client.new(username, password, user_agent, to_account_id, to_project_id)
log = Logger.new(STDOUT)
log.formatter = Logger::Formatter.new
Migrator.new(from, to, log).migrate
view raw run.rb hosted with ❤ by GitHub

It's not my best code, but whatever it works. The rescue/retry stuff was a last minute eye roller because I got an SSL error of sorts. Just keep an eye on it if it gets out of hand. Completed lists are migrated as well, so you have a record of that stuff too.

It migrated my 800+ todos in my Basecamp Next project to Basecamp Personal in about 24 minutes. Now I can save some bucks in the long run.

Happy migrating!