JumpstartLab/tracks

View on GitHub
doc/tracks_template_cli.rb

Summary

Maintainability
D
2 days
Test Coverage
#!/usr/bin/env ruby

# Version 0.4 (Dec 17, 2011)

#
# Based on the tracks_cli by Vitalie Lazu (https://gist.github.com/45537)
#

# CLI ruby template  client for Tracks: rails application for GTD methodology
# https://github.com/TracksApp/tracks

# Usage:
# * You need to set ENV['GTD_LOGIN'], ENV['GTD_PASSWORD']
# * You need to pipe a file with new projets and actions to the script
# * You can use the -k option to customize templates. See the example below.
#
# Default URLs are:
#    ENV['GTD_TODOS_URL']          --> 'http://localhost:3000/todos.xml'
#    ENV['GTD_PROJECTS_URL']       --> 'http://localhost:3000/projects.xml'
#    ENV['GTD_CONTEXT_URL_PREFIX'] --> 'http://localhost:3000/contexts/'
#    ENV['GTD_CONTEXT_URL']        --> 'http://localhost:3000/contexts.xml'

# project := <name>
# dependent_action := ^<name>|context|<tag1>,..|<notes>
# independent_action := .<name>|context|<tag1>,..|<notes>
# to star an action add a tag 'starred'

# Format of input file:
# - A token to be replaced in the subsequent lines starts with the string token
# - New Projects start at the beginning of the line
# - New actions start with an '.' or an '^' at the beginning of the line.
# - To add a note to an action, separate the action from its note by using '|'. You have to stay in the same line.
# - Comments start with '#'

# Simple test file. Remove the '# ' string at the beginning.
# token [A]
# token [BB]
# 
# to [A] after [BB]
# .task 1 in [A], [BB]|computer|starred,blue|my notes here
# ^task 1.1 dependent on [A]|||only a note
# .task 2
# 
# project 2 with [A]
# .task in project 2

# Example of an input file. Remove the '# ' string at the beginning and save it in a file.
# token [location]
# token [start]
# token [end]
# Book trip to [location]
# .Check visa requirements for [location]|starred|instantiate template_visa, if visa required
# .Book flight to [location]||starting trip around [start], returning around [end]
# .Print flight details to [location]
# .Book hotel in [location]|checking around [start], leaving around [end]
# .Book rental car in [location]|starting [start], returning [end]
# .Print hotel booking details to [location]
# .Set email vacation reminder|starting [start], returning [end]; Text: I'm off for a vacation. I'll respond to emails after returning ([end]).
# .Mail others that I'll be away|starting [start], returning [end]
# Pack stuff for trip to [location]
# .Pack projector laptop adapter
# .Pack socket adapter for country ([location])
# .Pack passport
# .Pack flight and hotel detail printout
# Get trip reimbursement for [location]
# .Collect all [location] receipts in a clear plastic folder
# .Set a reminder to check for reimbursement for [location]
# .Mail folder to secretary

# Instantiate this template: ./tracks_template_cli -c 1 -f template_file.txt

require 'net/https'
require 'optparse'
require 'cgi'
require 'time'
require 'readline'

class Hash
  def to_query_string
    map { |k, v|
      if v.instance_of?(Hash)
        v.map { |sk, sv|
          "#{k}[#{sk}]=#{sv}"
        }.join('&')
      else
        "#{k}=#{v}"
      end
    }.join('&')
  end
end

module Gtd
  class API
    GTD_URI_TODOS = ENV['GTD_TODOS_URL'] || 'http://localhost:3000/todos.xml'
    GTD_URI_PROJECTS = ENV['GTD_PROJECTS_URL'] || 'http://localhost:3000/projects.xml'
    GTD_URI_CONTEXTS_PREFIX = ENV['GTD_CONTEXT_URL_PREFIX'] || 'http://localhost:3000/contexts/'
    GTD_URI_CONTEXTS = ENV['GTD_CONTEXT_URL'] || 'http://localhost:3000/contexts.xml'

    def postTodo(l, options = {})
      uri = URI.parse(GTD_URI_TODOS)
      http = Net::HTTP.new(uri.host, uri.port)

      if uri.scheme == "https"  # enable SSL/TLS
        http.use_ssl = true
        http.ca_path = "/etc/ssl/certs/" # Debian based path
        http.verify_mode = OpenSSL::SSL::VERIFY_PEER
        http.verify_depth = 5
      end

      l.chomp!

      description = CGI.escapeHTML(l)
      context_id = options[:context_id] ? options[:context_id].to_i : 1
      project_id = options[:project_id] ? options[:project_id].to_i : 1
      props = "<description>#{description}</description><project_id>#{project_id}</project_id>"

      if options[:show_from]
        props << "<show-from type=\"datetime\">#{Time.at(options[:show_from]).xmlschema}</show-from>"
      end

      if options[:note]
        props << "<notes>#{options[:note]}</notes>"
      end

      if options[:taglist]
        tags = options[:taglist].split(",")
        if tags.length() > 0
          tags = tags.collect { |tag| "<tag><name>#{tag.strip}</name></tag>" unless tag.strip.empty?}.join('')
          props << "<tags>#{tags}</tags>"
        end
      end

      if not (options[:context].nil? || options[:context].empty?)
        props << "<context><name>#{options[:context]}</name></context>"
      else
        ## use the default context
        props << "<context_id>#{context_id}</context_id>"
      end
      
      if options[:depend]
        props << "<predecessor_dependencies><predecessor>#{options[:last_todo_id]}</predecessor></predecessor_dependencies>"
      end

      req = Net::HTTP::Post.new(uri.path, "Content-Type" => "text/xml")
      req.basic_auth ENV['GTD_LOGIN'], ENV['GTD_PASSWORD']
      req.body = "<todo>#{props}</todo>"

      puts req.body if options[:verbose]

      resp = http.request(req)

      if resp.code == '302' || resp.code == '201'
        puts resp['location'] if options[:verbose]
        
        # return the todo id
        return resp['location'].split("/").last
      else
        p resp.body
        raise Gtd::Error
      end
    end

    def postProject(l, options = {})
      uri = URI.parse(GTD_URI_PROJECTS)
      http = Net::HTTP.new(uri.host, uri.port)

      if uri.scheme == "https"  # enable SSL/TLS
        http.use_ssl = true
        http.ca_path = "/etc/ssl/certs/" # Debian based path
        http.verify_mode = OpenSSL::SSL::VERIFY_PEER
        http.verify_depth = 5
      end

      l.chomp!

      description = CGI.escapeHTML(l)
      props = "<name>#{l}</name><default-context-id>#{options[:context_id]}</default-context-id>"

      req = Net::HTTP::Post.new(uri.path, "Content-Type" => "text/xml")
      req.basic_auth ENV['GTD_LOGIN'], ENV['GTD_PASSWORD']
      req.body = "<project>#{props}</project>"

      resp = http.request(req)

      if resp.code == '302' || resp.code == '201'
        puts resp['location'] if options[:verbose]

        # return the project id
        return resp['location'].split("/").last
      else
        p resp.body
        raise Gtd::Error
      end
    end

    def queryContext(contextID)
      return false unless contextID.is_a? Integer

      uri = URI.parse(GTD_URI_CONTEXTS_PREFIX + contextID.to_s + ".xml")
      http = Net::HTTP.new(uri.host, uri.port)

      if uri.scheme == "https"  # enable SSL/TLS
        http.use_ssl = true
        http.ca_path = "/etc/ssl/certs/" # Debian based path
        http.verify_mode = OpenSSL::SSL::VERIFY_PEER
        http.verify_depth = 5
      end

      req = Net::HTTP::Get.new(uri.path)
      req.basic_auth ENV['GTD_LOGIN'], ENV['GTD_PASSWORD']
      resp = http.request(req)

      case resp
      when Net::HTTPSuccess
        return true
      else
        return false
      end
    end

  end


  class Error < StandardError; end
  class InvalidParser < StandardError; end

  class ConsoleOptions
    attr_reader :parser, :options, :keywords

    def initialize
      @options = {}
      @keywords = {}

      @parser = OptionParser.new do |cmd|
        cmd.banner = "Ruby Gtd Templates CLI"

        cmd.separator ''

        cmd.on('-h', '--help', 'Displays this help message') do
          puts @parser
          exit
        end

        cmd.on('-p [N]', Integer, "project id to set for new todo") do |v|
          @options[:project_id] = v
        end

        cmd.on('-k [S]', "keyword to be replaced") do |v|
          @keywords[v.split("=")[0]] = v.split("=")[1]
        end

        cmd.on('-v', "verbose on") do |v|
          @options[:verbose] = true
        end

        cmd.on('-f [S]', "filename of the template") do |v|
          @filename = v
        end

        cmd.on('-c [N]', Integer, 'default context id to set for new projects') do |v|
          @options[:context_id] = v
        end

        cmd.on('-w [N]', Integer, 'Postpone task for N weeks') do |v|
          @options[:show_from] = Time.now.to_i + 24 * 3600 * 7 * (v || 1)
        end

        cmd.on('-m [N]', Integer, 'Postpone task for N months') do |v|
          @options[:show_from] = Time.now.to_i + 24 * 3600 * 7 * 4 * (v || 1)
        end
      end
    end

    def run(args)
      @parser.parse!(args)
      # lines = STDIN.read
      gtd = API.new

      if @filename != nil and not File.exist?(@filename)
        puts "ERROR: file #{@filename} doesn't exist"
        exit 1
      end

      if ENV['GTD_LOGIN'] == nil
        puts "ERROR: no GTD_LOGIN environment variable set"
        exit 1
      end
      
      if ENV['GTD_PASSWORD'] == nil
        puts "ERROR: no GTD_PASSWORD environment variable set"
        exit 1
      end
      
      if @filename == nil
        file = STDIN
      else
        file = File.open(@filename)
      end
      
      ## check for existence of the context
      if !@options[:context_id]
        puts "ERROR: need to specify a context_id with -c option. Go here to find one: #{API::GTD_URI_CONTEXTS}"
        exit 1
      end

      if !gtd.queryContext(@options[:context_id])
        puts "Error: context_id #{options[:context_id]} doesn't exist"
        exit 1
      end

      #lines.each_line do |line|
      while line = file.gets
        line = line.strip
        next if (line.empty? || line[0].chr == "#")

        if (line.split(' ')[0] == "token") 
          ## defining a new token; ask for input

          newtok=line.split(' ')[1]

          print "Input required for "+newtok+": "
          @keywords[newtok]=gets.chomp
          next
        end

        # replace tokens
        @keywords.each do |key,val|
          line=line.sub(key,val)
        end

        # decide whether project or task
        if (line[0].chr == "." ) || (line[0].chr == "^")
          @options[:depend]= line[0].chr == "^" ? true : false;
          line = line[1..line.length]

          # find notes
          tmp= line.split("|")
          if tmp.length > 5
            puts "Formatting error: found too many |"
            exit 1
          end

          line=tmp[0] 

          tmp.each_with_index do |t,idx| 
            t=t.strip.chomp
            t=nil if t.empty?
            tmp[idx]=t
          end

          @options[:context]=tmp[1]
          @options[:taglist]=tmp[2]
          @options[:note]=tmp[3]

          if !@options[:project_id]
            puts "Warning: no project specified for task \"#{line}\". Using default project."
          end

          @options[:last_todo_id]=gtd.postTodo(line, @options)
        else
          @options[:project_id]=gtd.postProject(line, @options)
        end
      end

      exit 0
    rescue InvalidParser
      puts "Please specify a valid format parser."
      exit 1
    rescue Error
      puts "An unknown error occurred"
      exit 1
    end
  end
end

if $0 == __FILE__
  app = Gtd::ConsoleOptions.new
  app.run(ARGV)
end