asanban 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
 - data/asanban.gemspec +1 -0
 - data/lib/asanban.rb +2 -1
 - data/lib/asanban/bulk_load.rb +148 -138
 - data/lib/asanban/service.rb +122 -55
 - data/lib/asanban/version.rb +1 -1
 - data/spec/service_spec.rb +148 -56
 - data/static/dashboard.html +44 -0
 - data/static/js/controllers.js +28 -0
 - metadata +18 -2
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,15 +1,15 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            !binary "U0hBMQ==":
         
     | 
| 
       3 
3 
     | 
    
         
             
              metadata.gz: !binary |-
         
     | 
| 
       4 
     | 
    
         
            -
                 
     | 
| 
      
 4 
     | 
    
         
            +
                Y2VmZjRhNTI3ZGE1OTUyZjYzYWE1OTliNDU1MmFlZWViNWU0OGIwMQ==
         
     | 
| 
       5 
5 
     | 
    
         
             
              data.tar.gz: !binary |-
         
     | 
| 
       6 
     | 
    
         
            -
                 
     | 
| 
      
 6 
     | 
    
         
            +
                MTkxMjA5OGQzMGM5NzBlODFiNjMyNmE2MDdmNDI2OTA2MTVlYzAwYQ==
         
     | 
| 
       7 
7 
     | 
    
         
             
            SHA512:
         
     | 
| 
       8 
8 
     | 
    
         
             
              metadata.gz: !binary |-
         
     | 
| 
       9 
     | 
    
         
            -
                 
     | 
| 
       10 
     | 
    
         
            -
                 
     | 
| 
       11 
     | 
    
         
            -
                 
     | 
| 
      
 9 
     | 
    
         
            +
                ZjRhMTdkYjdhYmNmNTFkYzJkM2NmYjIwY2M1MjE5ZmVhNzFiMThlODc4M2Zi
         
     | 
| 
      
 10 
     | 
    
         
            +
                MWMyODJjNjYyY2I4MzY3NjY2ZjJlMTJjMDJkNGI5ZjgxNDBiNjU1ZDY0N2Ni
         
     | 
| 
      
 11 
     | 
    
         
            +
                YzAyMTU5OTBlZmJjYTExNDg3MWY1YzAzY2YxZTM5MmRlYjEwM2I=
         
     | 
| 
       12 
12 
     | 
    
         
             
              data.tar.gz: !binary |-
         
     | 
| 
       13 
     | 
    
         
            -
                 
     | 
| 
       14 
     | 
    
         
            -
                 
     | 
| 
       15 
     | 
    
         
            -
                 
     | 
| 
      
 13 
     | 
    
         
            +
                NGY3MDViYTU2ZDA0NDZjMDlkM2Y0ZjljNjUxODVlYjA5YWExZTAzN2JhYzI3
         
     | 
| 
      
 14 
     | 
    
         
            +
                ODE5NDZkZThlMmJjMjA0ZWE4MDdiMTJkNWJlNzBjYTQ0MDk2NDM3Yzk4NDRi
         
     | 
| 
      
 15 
     | 
    
         
            +
                ZTk0ZmQwOTYxOWQ2MDExOWYyOTYxY2MwNjhhNTBlZjk3ODNjYTI=
         
     | 
    
        data/asanban.gemspec
    CHANGED
    
    | 
         @@ -20,6 +20,7 @@ Gem::Specification.new do |gem| 
     | 
|
| 
       20 
20 
     | 
    
         
             
              gem.add_dependency "json"
         
     | 
| 
       21 
21 
     | 
    
         
             
              gem.add_dependency "json_pure"
         
     | 
| 
       22 
22 
     | 
    
         
             
              gem.add_dependency "sinatra"
         
     | 
| 
      
 23 
     | 
    
         
            +
              gem.add_dependency "descriptive_statistics"
         
     | 
| 
       23 
24 
     | 
    
         
             
              gem.add_development_dependency "rspec"
         
     | 
| 
       24 
25 
     | 
    
         
             
              gem.add_development_dependency "rack-test"
         
     | 
| 
       25 
26 
     | 
    
         
             
            end
         
     | 
    
        data/lib/asanban.rb
    CHANGED
    
    
    
        data/lib/asanban/bulk_load.rb
    CHANGED
    
    | 
         @@ -6,156 +6,166 @@ require "time" 
     | 
|
| 
       6 
6 
     | 
    
         
             
            require "yaml"
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            module Asanban
         
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
      
 9 
     | 
    
         
            +
              class BulkLoad
         
     | 
| 
      
 10 
     | 
    
         
            +
                def self.run(config = nil)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  unless config
         
     | 
| 
      
 12 
     | 
    
         
            +
                    if !File.exists?('asana.yml')
         
     | 
| 
      
 13 
     | 
    
         
            +
                      puts "Must specify configuration in asana.yml"
         
     | 
| 
      
 14 
     | 
    
         
            +
                      exit(1)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
                    config = YAML::load(File.open('asana.yml'))
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
                  api_key = config['asana_api_key']
         
     | 
| 
      
 19 
     | 
    
         
            +
                  workspace_id = config['asana_workspace_id']
         
     | 
| 
      
 20 
     | 
    
         
            +
                  project_id = config['asana_project_id']
         
     | 
| 
      
 21 
     | 
    
         
            +
                  mongodb_uri = config['mongodb_uri']
         
     | 
| 
      
 22 
     | 
    
         
            +
                  mongodb_dbname = config['mongodb_dbname']
         
     | 
| 
       23 
23 
     | 
    
         | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
             
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
       41 
     | 
    
         
            -
             
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
      
 24 
     | 
    
         
            +
                  if (ARGV.count < 1 || ARGV.count > 2)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    puts "Syntax is: bulkload {stage} [mode]"
         
     | 
| 
      
 26 
     | 
    
         
            +
                    puts "{stage} is 'LOCAL' or 'PROD'"
         
     | 
| 
      
 27 
     | 
    
         
            +
                    puts "[mode] can be 'tasks' (to create task data) or 'times' to create milestone and lead time data. Script will do both if not specified."
         
     | 
| 
      
 28 
     | 
    
         
            +
                    exit(1)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  else
         
     | 
| 
      
 30 
     | 
    
         
            +
                    if (ARGV[0].downcase == "local")
         
     | 
| 
      
 31 
     | 
    
         
            +
                      conn = Mongo::Connection.new
         
     | 
| 
      
 32 
     | 
    
         
            +
                    elsif (ARGV[0].downcase == "prod")
         
     | 
| 
      
 33 
     | 
    
         
            +
                      conn = Mongo::Connection.from_uri(mongodb_uri)
         
     | 
| 
      
 34 
     | 
    
         
            +
                    else
         
     | 
| 
      
 35 
     | 
    
         
            +
                      puts "Invalid stage: #{ARGV[0]}"
         
     | 
| 
      
 36 
     | 
    
         
            +
                      exit(1)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
                    if (mode = ARGV[1]) && !['tasks', 'times'].include?(mode)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      puts "Invalid mode: #{mode}"
         
     | 
| 
      
 40 
     | 
    
         
            +
                      exit(1)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
       43 
43 
     | 
    
         | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
       45 
     | 
    
         
            -
             
     | 
| 
      
 44 
     | 
    
         
            +
                  db = conn.db(mongodb_dbname)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  tasks_collection = db["tasks"]
         
     | 
| 
       46 
46 
     | 
    
         | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
      
 47 
     | 
    
         
            +
                  if (mode == 'tasks' || !mode)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    puts "Creating times..."
         
     | 
| 
      
 49 
     | 
    
         
            +
                    uri = URI.parse("https://app.asana.com/api/1.0/projects/#{project_id}/tasks?opt_fields=id,name,assignee,assignee_status,created_at,completed,completed_at,due_on,followers,modified_at,name,notes,projects,parent")
         
     | 
| 
      
 50 
     | 
    
         
            +
                    http = Net::HTTP.new(uri.host, uri.port)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    http.use_ssl = true
         
     | 
| 
      
 52 
     | 
    
         
            +
                    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
         
     | 
| 
      
 53 
     | 
    
         
            +
                    header = { "Content-Type" => "application/json" }
         
     | 
| 
      
 54 
     | 
    
         
            +
                    path = "#{uri.path}?#{uri.query}"
         
     | 
| 
      
 55 
     | 
    
         
            +
                    req = Net::HTTP::Get.new(path, header)
         
     | 
| 
      
 56 
     | 
    
         
            +
                    req.basic_auth(api_key, '')
         
     | 
| 
      
 57 
     | 
    
         
            +
                    res = http.start { |http| http.request(req) }
         
     | 
| 
      
 58 
     | 
    
         
            +
                    tasks = JSON.parse(res.body)
         
     | 
| 
       59 
59 
     | 
    
         | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
             
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
             
     | 
| 
       68 
     | 
    
         
            -
             
     | 
| 
       69 
     | 
    
         
            -
             
     | 
| 
      
 60 
     | 
    
         
            +
                    if tasks['errors']
         
     | 
| 
      
 61 
     | 
    
         
            +
                      puts "Server returned an error retrieving tasks: #{tasks['errors'][0]['message']}"
         
     | 
| 
      
 62 
     | 
    
         
            +
                    else
         
     | 
| 
      
 63 
     | 
    
         
            +
                      #old_tasks = tasks_collection.find({"projects.id" => project_id, "completed" => false}).to_a
         
     | 
| 
      
 64 
     | 
    
         
            +
                      tasks_collection.update({}, {"$set" => {"old" => true}}, {:multi => true})
         
     | 
| 
      
 65 
     | 
    
         
            +
                      tasks['data'].each_with_index do |task, i|
         
     | 
| 
      
 66 
     | 
    
         
            +
                        uri = URI.parse("https://app.asana.com/api/1.0/tasks/#{task['id']}/stories")
         
     | 
| 
      
 67 
     | 
    
         
            +
                        req = Net::HTTP::Get.new(uri.path, header)
         
     | 
| 
      
 68 
     | 
    
         
            +
                        req.basic_auth(api_key, '')
         
     | 
| 
      
 69 
     | 
    
         
            +
                        res = http.start { |http| http.request(req) }
         
     | 
| 
      
 70 
     | 
    
         
            +
                        stories = JSON.parse(res.body)
         
     | 
| 
       70 
71 
     | 
    
         | 
| 
       71 
     | 
    
         
            -
             
     | 
| 
       72 
     | 
    
         
            -
             
     | 
| 
       73 
     | 
    
         
            -
             
     | 
| 
       74 
     | 
    
         
            -
             
     | 
| 
       75 
     | 
    
         
            -
             
     | 
| 
      
 72 
     | 
    
         
            +
                        if task['errors']
         
     | 
| 
      
 73 
     | 
    
         
            +
                          puts "Server returned an error retrieving stories: #{task['errors'][0]['message']}"
         
     | 
| 
      
 74 
     | 
    
         
            +
                        else
         
     | 
| 
      
 75 
     | 
    
         
            +
                         task["stories"] = stories['data']
         
     | 
| 
      
 76 
     | 
    
         
            +
                        end
         
     | 
| 
       76 
77 
     | 
    
         | 
| 
       77 
     | 
    
         
            -
             
     | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
      
 78 
     | 
    
         
            +
                        task["old"] = false
         
     | 
| 
      
 79 
     | 
    
         
            +
                        tasks_collection.update({"id" => task['id']}, task, {:upsert => true})
         
     | 
| 
      
 80 
     | 
    
         
            +
                        puts "Created task: #{task['id']}"
         
     | 
| 
       79 
81 
     | 
    
         | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
             
     | 
| 
       82 
     | 
    
         
            -
             
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
       84 
     | 
    
         
            -
             
     | 
| 
      
 82 
     | 
    
         
            +
                        if (((i + 1) % 100) == 0)
         
     | 
| 
      
 83 
     | 
    
         
            +
                          puts "Sleeping for one minute to avoid Asana's rate limit of 100 requests per minute"
         
     | 
| 
      
 84 
     | 
    
         
            +
                          sleep 60
         
     | 
| 
      
 85 
     | 
    
         
            +
                        end
         
     | 
| 
      
 86 
     | 
    
         
            +
                      end
         
     | 
| 
       85 
87 
     | 
    
         | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
             
     | 
| 
       88 
     | 
    
         
            -
             
     | 
| 
      
 88 
     | 
    
         
            +
                      puts "Done creating times."
         
     | 
| 
      
 89 
     | 
    
         
            +
                    end
         
     | 
| 
      
 90 
     | 
    
         
            +
                  end
         
     | 
| 
       89 
91 
     | 
    
         | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
             
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
     | 
    
         
            -
             
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
             
     | 
| 
       106 
     | 
    
         
            -
             
     | 
| 
      
 92 
     | 
    
         
            +
                  if (mode == 'times' || !mode)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    puts "Creating milestone and lead time data..."
         
     | 
| 
      
 94 
     | 
    
         
            +
                    milestone_times_collection = db["milestone_times"]
         
     | 
| 
      
 95 
     | 
    
         
            +
                    lead_times_collection = db["lead_times"]
         
     | 
| 
      
 96 
     | 
    
         
            +
                    # TODO: filter - complete_time = null or completed_time - now <= 24hours
         
     | 
| 
      
 97 
     | 
    
         
            +
                    tasks_collection.find().each do |task|
         
     | 
| 
      
 98 
     | 
    
         
            +
                      stories = task["stories"] || []
         
     | 
| 
      
 99 
     | 
    
         
            +
                      task_id = task["_id"]
         
     | 
| 
      
 100 
     | 
    
         
            +
                      #TODO: Write a test...
         
     | 
| 
      
 101 
     | 
    
         
            +
                      task_completed = task["completed"]
         
     | 
| 
      
 102 
     | 
    
         
            +
                      task_deleted = task["old"]
         
     | 
| 
      
 103 
     | 
    
         
            +
                      stories.each do |story|
         
     | 
| 
      
 104 
     | 
    
         
            +
                        if (story['text'] =~ /Moved from (.*)\(\d+\) to (.*)\(\d+\)/)
         
     | 
| 
      
 105 
     | 
    
         
            +
                          start_milestone = $1.strip
         
     | 
| 
      
 106 
     | 
    
         
            +
                          end_milestone = $2.strip
         
     | 
| 
      
 107 
     | 
    
         
            +
                          timestamp = Time.parse(story["created_at"])
         
     | 
| 
      
 108 
     | 
    
         
            +
                          day = "#{timestamp.year}-#{timestamp.month}-#{timestamp.day}"
         
     | 
| 
      
 109 
     | 
    
         
            +
                          month = "#{timestamp.year}-#{timestamp.month}"
         
     | 
| 
      
 110 
     | 
    
         
            +
                          year = timestamp.year.to_s
         
     | 
| 
      
 111 
     | 
    
         
            +
                          end_story_id = story["id"]
         
     | 
| 
      
 112 
     | 
    
         
            +
                          escaped_milestone = start_milestone.gsub('(', '\\(').gsub(')', '\\)')
         
     | 
| 
       107 
113 
     | 
    
         | 
| 
       108 
     | 
    
         
            -
             
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
             
     | 
| 
       111 
     | 
    
         
            -
             
     | 
| 
       112 
     | 
    
         
            -
             
     | 
| 
       113 
     | 
    
         
            -
             
     | 
| 
      
 114 
     | 
    
         
            +
                          if (start_story = stories.find {|s| s['text'] =~ /Moved .*to #{escaped_milestone}/})
         
     | 
| 
      
 115 
     | 
    
         
            +
                            #TODO: Refactor to use record_time
         
     | 
| 
      
 116 
     | 
    
         
            +
                            start_story_id = start_story["id"]
         
     | 
| 
      
 117 
     | 
    
         
            +
                            start_timestamp = Time.parse(start_story["created_at"])
         
     | 
| 
      
 118 
     | 
    
         
            +
                            elapsed_time_seconds = timestamp - start_timestamp
         
     | 
| 
      
 119 
     | 
    
         
            +
                            elapsed_days = elapsed_time_seconds / (60.0 * 60.0 * 24.0)
         
     | 
| 
       114 
120 
     | 
    
         | 
| 
       115 
     | 
    
         
            -
             
     | 
| 
       116 
     | 
    
         
            -
             
     | 
| 
       117 
     | 
    
         
            -
             
     | 
| 
       118 
     | 
    
         
            -
             
     | 
| 
       119 
     | 
    
         
            -
             
     | 
| 
       120 
     | 
    
         
            -
             
     | 
| 
       121 
     | 
    
         
            -
             
     | 
| 
       122 
     | 
    
         
            -
             
     | 
| 
       123 
     | 
    
         
            -
             
     | 
| 
       124 
     | 
    
         
            -
             
     | 
| 
       125 
     | 
    
         
            -
             
     | 
| 
       126 
     | 
    
         
            -
             
     | 
| 
      
 121 
     | 
    
         
            +
                            milestone = {"day" => day, "month" => month, "year" => year, 
         
     | 
| 
      
 122 
     | 
    
         
            +
                              "task_id" => task_id, "date" => Time.parse(day),
         
     | 
| 
      
 123 
     | 
    
         
            +
                              "task_completed" => task_completed, "task_deleted" => task_deleted,
         
     | 
| 
      
 124 
     | 
    
         
            +
                              "start_milestone" => start_milestone, "end_milestone" => end_milestone, 
         
     | 
| 
      
 125 
     | 
    
         
            +
                              "start_story_id" => start_story_id, "end_story_id" => end_story_id,
         
     | 
| 
      
 126 
     | 
    
         
            +
                              "elapsed_days" => elapsed_days}
         
     | 
| 
      
 127 
     | 
    
         
            +
                            milestone_times_collection.remove("end_story_id" => end_story_id)
         
     | 
| 
      
 128 
     | 
    
         
            +
                            milestone_times_collection.insert(milestone)
         
     | 
| 
      
 129 
     | 
    
         
            +
                            puts "Inserted milestone: #{milestone}"
         
     | 
| 
      
 130 
     | 
    
         
            +
                          else
         
     | 
| 
      
 131 
     | 
    
         
            +
                            puts "Could not find time task entered #{start_milestone}"
         
     | 
| 
      
 132 
     | 
    
         
            +
                          end
         
     | 
| 
      
 133 
     | 
    
         
            +
                        end
         
     | 
| 
      
 134 
     | 
    
         
            +
                      end
         
     | 
| 
       127 
135 
     | 
    
         | 
| 
       128 
     | 
    
         
            -
             
     | 
| 
       129 
     | 
    
         
            -
             
     | 
| 
       130 
     | 
    
         
            -
             
     | 
| 
       131 
     | 
    
         
            -
             
     | 
| 
       132 
     | 
    
         
            -
             
     | 
| 
       133 
     | 
    
         
            -
             
     | 
| 
       134 
     | 
    
         
            -
             
     | 
| 
       135 
     | 
    
         
            -
             
     | 
| 
       136 
     | 
    
         
            -
             
     | 
| 
       137 
     | 
    
         
            -
             
     | 
| 
       138 
     | 
    
         
            -
             
     | 
| 
      
 136 
     | 
    
         
            +
                      if ((end_story = stories.find_all {|s| s['text'] =~ /Moved .*to #{config['asana_ending_milestone']}/}[-1]) && 
         
     | 
| 
      
 137 
     | 
    
         
            +
                          (start_story = stories.find_all {|s| s['text'] =~ /Moved .*to #{config['asana_beginning_milestone']}/}[0]))
         
     | 
| 
      
 138 
     | 
    
         
            +
                        lead_times_collection.remove("end_story_id" => end_story["id"])
         
     | 
| 
      
 139 
     | 
    
         
            +
                        lead_time = record_time(task_id, task_completed, task_deleted, start_story, config['asana_beginning_milestone'], end_story, config['asana_ending_milestone'], lead_times_collection)
         
     | 
| 
      
 140 
     | 
    
         
            +
                        puts "Inserted lead time: #{lead_time}"
         
     | 
| 
      
 141 
     | 
    
         
            +
                      end
         
     | 
| 
      
 142 
     | 
    
         
            +
                    end
         
     | 
| 
      
 143 
     | 
    
         
            +
                    puts "Finished creating milestone and lead time data."
         
     | 
| 
      
 144 
     | 
    
         
            +
                  end
         
     | 
| 
      
 145 
     | 
    
         
            +
                  puts "Done!"
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
       139 
147 
     | 
    
         | 
| 
       140 
     | 
    
         
            -
             
     | 
| 
       141 
     | 
    
         
            -
             
     | 
| 
       142 
     | 
    
         
            -
             
     | 
| 
       143 
     | 
    
         
            -
             
     | 
| 
       144 
     | 
    
         
            -
             
     | 
| 
       145 
     | 
    
         
            -
             
     | 
| 
       146 
     | 
    
         
            -
             
     | 
| 
       147 
     | 
    
         
            -
             
     | 
| 
       148 
     | 
    
         
            -
             
     | 
| 
       149 
     | 
    
         
            -
             
     | 
| 
       150 
     | 
    
         
            -
             
     | 
| 
      
 148 
     | 
    
         
            +
                #Visible For Testing
         
     | 
| 
      
 149 
     | 
    
         
            +
                def self.record_time(task_id, task_completed, task_deleted, start_story, start_milestone, end_story, end_milestone, collection)
         
     | 
| 
      
 150 
     | 
    
         
            +
                  end_story_id = end_story["id"]
         
     | 
| 
      
 151 
     | 
    
         
            +
                  start_story_id = start_story["id"]
         
     | 
| 
      
 152 
     | 
    
         
            +
                  start_timestamp = Time.parse(start_story["created_at"])
         
     | 
| 
      
 153 
     | 
    
         
            +
                  end_timestamp = Time.parse(end_story["created_at"])
         
     | 
| 
      
 154 
     | 
    
         
            +
                  elapsed_time_seconds = end_timestamp - start_timestamp
         
     | 
| 
      
 155 
     | 
    
         
            +
                  elapsed_days = elapsed_time_seconds / (60.0 * 60.0 * 24.0)
         
     | 
| 
      
 156 
     | 
    
         
            +
                  day = "#{end_timestamp.year}-#{end_timestamp.month}-#{end_timestamp.day}"
         
     | 
| 
      
 157 
     | 
    
         
            +
                  month = "#{end_timestamp.year}-#{end_timestamp.month}"
         
     | 
| 
      
 158 
     | 
    
         
            +
                  year = end_timestamp.year.to_s
         
     | 
| 
       151 
159 
     | 
    
         | 
| 
       152 
     | 
    
         
            -
             
     | 
| 
       153 
     | 
    
         
            -
             
     | 
| 
       154 
     | 
    
         
            -
             
     | 
| 
       155 
     | 
    
         
            -
             
     | 
| 
       156 
     | 
    
         
            -
             
     | 
| 
       157 
     | 
    
         
            -
             
     | 
| 
       158 
     | 
    
         
            -
             
     | 
| 
       159 
     | 
    
         
            -
             
     | 
| 
       160 
     | 
    
         
            -
             
     | 
| 
      
 160 
     | 
    
         
            +
                  record = {"day" => day, "month" => month, "year" => year, 
         
     | 
| 
      
 161 
     | 
    
         
            +
                    "task_id" => task_id, "date" => Time.parse(day),
         
     | 
| 
      
 162 
     | 
    
         
            +
                    "task_completed" => task_completed, "task_deleted" => task_deleted,
         
     | 
| 
      
 163 
     | 
    
         
            +
                    "start_milestone" => start_milestone, "end_milestone" => end_milestone, 
         
     | 
| 
      
 164 
     | 
    
         
            +
                    "start_story_id" => start_story_id, "end_story_id" => end_story_id,
         
     | 
| 
      
 165 
     | 
    
         
            +
                    "elapsed_days" => elapsed_days}
         
     | 
| 
      
 166 
     | 
    
         
            +
                  collection.remove("end_story_id" => end_story_id)
         
     | 
| 
      
 167 
     | 
    
         
            +
                  collection.insert(record)
         
     | 
| 
      
 168 
     | 
    
         
            +
                  record
         
     | 
| 
      
 169 
     | 
    
         
            +
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
              end
         
     | 
| 
       161 
171 
     | 
    
         
             
            end
         
     | 
    
        data/lib/asanban/service.rb
    CHANGED
    
    | 
         @@ -3,61 +3,128 @@ require "json" 
     | 
|
| 
       3 
3 
     | 
    
         
             
            require "mongo"
         
     | 
| 
       4 
4 
     | 
    
         
             
            require "sinatra"
         
     | 
| 
       5 
5 
     | 
    
         
             
            require "yaml"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require "descriptive_statistics"
         
     | 
| 
       6 
7 
     | 
    
         | 
| 
       7 
8 
     | 
    
         
             
            module Asanban
         
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
             
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
       41 
     | 
    
         
            -
             
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
       45 
     | 
    
         
            -
             
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
             
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
      
 9 
     | 
    
         
            +
              class Service < Sinatra::Base
         
     | 
| 
      
 10 
     | 
    
         
            +
                set :public_folder, File.expand_path(File.join('..', '..', '..', 'static'), __FILE__)
         
     | 
| 
      
 11 
     | 
    
         
            +
                set :port, ENV['PORT'] if ENV['PORT'] #set by Heroku
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                configure :production, :development do
         
     | 
| 
      
 14 
     | 
    
         
            +
                  if !File.exists?('asana.yml')
         
     | 
| 
      
 15 
     | 
    
         
            +
                    puts "Must specify configuration in asana.yml"
         
     | 
| 
      
 16 
     | 
    
         
            +
                    exit(1)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
                  set :config, YAML::load(File.open('asana.yml'))
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                get '/metrics' do
         
     | 
| 
      
 22 
     | 
    
         
            +
                  config = settings.config
         
     | 
| 
      
 23 
     | 
    
         
            +
                  conn = Mongo::Connection.from_uri(config['mongodb_uri'])
         
     | 
| 
      
 24 
     | 
    
         
            +
                  db = conn.db(config["mongodb_dbname"])
         
     | 
| 
      
 25 
     | 
    
         
            +
                  aggregate_by = params[:aggregate_by]
         
     | 
| 
      
 26 
     | 
    
         
            +
                  return [400, "Cannot aggregate by #{aggregate_by}"] unless ["year", "month", "day", "start_milestone"].include? aggregate_by
         
     | 
| 
      
 27 
     | 
    
         
            +
                  content_type :json
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  map_function = "function() { emit(this.#{aggregate_by}, { count: 1, elapsed_days: this.elapsed_days }); };"
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  reduce_function = "function (name, values){
         
     | 
| 
      
 32 
     | 
    
         
            +
                    var n = {count : 0, elapsed_days : 0};
         
     | 
| 
      
 33 
     | 
    
         
            +
                    for ( var i=0; i<values.length; i++ ){
         
     | 
| 
      
 34 
     | 
    
         
            +
                      n.count += values[i].count;
         
     | 
| 
      
 35 
     | 
    
         
            +
                      n.elapsed_days += values[i].elapsed_days;
         
     | 
| 
      
 36 
     | 
    
         
            +
                    }
         
     | 
| 
      
 37 
     | 
    
         
            +
                    return n;
         
     | 
| 
      
 38 
     | 
    
         
            +
                  };"
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  finalize_function = "function(who, res){
         
     | 
| 
      
 41 
     | 
    
         
            +
                    res.avg = res.elapsed_days / res.count;
         
     | 
| 
      
 42 
     | 
    
         
            +
                    return res;
         
     | 
| 
      
 43 
     | 
    
         
            +
                  };"
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  map_reduce_options = {:finalize => finalize_function, :out => "mr_results"}
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                  if (aggregate_by == "start_milestone")
         
     | 
| 
      
 48 
     | 
    
         
            +
                    results = db["milestone_times"].map_reduce(map_function, reduce_function, map_reduce_options)
         
     | 
| 
      
 49 
     | 
    
         
            +
                    hashes = results.find().map do |result|
         
     | 
| 
      
 50 
     | 
    
         
            +
                      {result['_id'] => 
         
     | 
| 
      
 51 
     | 
    
         
            +
                        {"count" => result["value"]["count"], 
         
     | 
| 
      
 52 
     | 
    
         
            +
                          "cycle_time" => result["value"]["avg"]}}
         
     | 
| 
      
 53 
     | 
    
         
            +
                    end
         
     | 
| 
      
 54 
     | 
    
         
            +
                    #TODO: Refactor
         
     | 
| 
      
 55 
     | 
    
         
            +
                    hash = {}
         
     | 
| 
      
 56 
     | 
    
         
            +
                    hashes.map do |h|
         
     | 
| 
      
 57 
     | 
    
         
            +
                      hash.merge! h
         
     | 
| 
      
 58 
     | 
    
         
            +
                    end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    #TODO: Move this (and other M/Rs?) to bulk loader
         
     | 
| 
      
 61 
     | 
    
         
            +
                    map_function = "function() { emit(this.task_id, {end_milestone: this.end_milestone, end_story_id: this.end_story_id, day: this.day}); };"
         
     | 
| 
      
 62 
     | 
    
         
            +
                    reduce_function = "function (name, values){
         
     | 
| 
      
 63 
     | 
    
         
            +
                      var n = {end_milestone : '', end_story_id : 0};
         
     | 
| 
      
 64 
     | 
    
         
            +
                      for ( var i=0; i<values.length; i++ ){
         
     | 
| 
      
 65 
     | 
    
         
            +
                        if (values[i].end_story_id > n.end_story_id) {
         
     | 
| 
      
 66 
     | 
    
         
            +
                          n.end_milestone = values[i].end_milestone;
         
     | 
| 
      
 67 
     | 
    
         
            +
                          n.end_story_id = values[i].end_story_id;
         
     | 
| 
      
 68 
     | 
    
         
            +
                          n.day = values[i].day;
         
     | 
| 
      
 69 
     | 
    
         
            +
                        }
         
     | 
| 
      
 70 
     | 
    
         
            +
                      }
         
     | 
| 
      
 71 
     | 
    
         
            +
                      return n;
         
     | 
| 
      
 72 
     | 
    
         
            +
                    };"
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    query = {"task_completed" => false, "task_deleted" => false}
         
     | 
| 
      
 75 
     | 
    
         
            +
                    if ((start_date = params[:start_date]) && (end_date = params[:end_date]))
         
     | 
| 
      
 76 
     | 
    
         
            +
                      query["date"] = {"$gte" => Time.parse(start_date), "$lte" => Time.parse(end_date)}
         
     | 
| 
      
 77 
     | 
    
         
            +
                    end
         
     | 
| 
      
 78 
     | 
    
         
            +
                    results = db["milestone_times"].map_reduce(map_function, reduce_function, :query => query, :out => "mr_end_milestone")
         
     | 
| 
      
 79 
     | 
    
         
            +
                    elapsed_days_by_phase = {}
         
     | 
| 
      
 80 
     | 
    
         
            +
                    results.find().map do |result|
         
     | 
| 
      
 81 
     | 
    
         
            +
                      task_id = result['_id']
         
     | 
| 
      
 82 
     | 
    
         
            +
                      end_milestone = result["value"]["end_milestone"]
         
     | 
| 
      
 83 
     | 
    
         
            +
                      if (end_milestone == "Dev Ready (10): Strat(5), Eng (1), Imp(3), Eme")
         
     | 
| 
      
 84 
     | 
    
         
            +
                        puts "task_id: #{task_id}"
         
     | 
| 
      
 85 
     | 
    
         
            +
                      end
         
     | 
| 
      
 86 
     | 
    
         
            +
                      day = result["value"]["day"]
         
     | 
| 
      
 87 
     | 
    
         
            +
                      milestone_metrics = (hash[end_milestone] ||= {})
         
     | 
| 
      
 88 
     | 
    
         
            +
                      milestone_metrics["current"] ||= 0
         
     | 
| 
      
 89 
     | 
    
         
            +
                      milestone_metrics["current"] += 1
         
     | 
| 
      
 90 
     | 
    
         
            +
                      elapsed_seconds = Time.now - Time.parse(day)
         
     | 
| 
      
 91 
     | 
    
         
            +
                      elapsed_days = ((elapsed_seconds / 60) / 60) / 24
         
     | 
| 
      
 92 
     | 
    
         
            +
                      milestone_metrics["current_days_total"] ||= 0
         
     | 
| 
      
 93 
     | 
    
         
            +
                      milestone_metrics["current_days_total"] += elapsed_days
         
     | 
| 
      
 94 
     | 
    
         
            +
                      elapsed_days_by_phase[end_milestone] ||= []
         
     | 
| 
      
 95 
     | 
    
         
            +
                      elapsed_days_by_phase[end_milestone].push elapsed_days
         
     | 
| 
      
 96 
     | 
    
         
            +
                    end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    hash.each do |key, value|
         
     | 
| 
      
 99 
     | 
    
         
            +
                      if (value["current_days_total"] && value["current"])
         
     | 
| 
      
 100 
     | 
    
         
            +
                        value["current_days_average"] = value["current_days_total"] / value["current"]
         
     | 
| 
      
 101 
     | 
    
         
            +
                        all_elapsed_days = elapsed_days_by_phase[key]
         
     | 
| 
      
 102 
     | 
    
         
            +
                        value["current_days_stdev"] = all_elapsed_days.standard_deviation
         
     | 
| 
      
 103 
     | 
    
         
            +
                      end
         
     | 
| 
      
 104 
     | 
    
         
            +
                    end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                    if (params[:current_milestones_only])
         
     | 
| 
      
 107 
     | 
    
         
            +
                      hash = hash.select {|phase, phase_metrics| phase_metrics["current_days_average"] }
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
                    return hash.to_json
         
     | 
| 
      
 110 
     | 
    
         
            +
                  end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                  if (milestone = params[:milestone])
         
     | 
| 
      
 113 
     | 
    
         
            +
                    map_reduce_options[:query] = {"start_milestone" => milestone}
         
     | 
| 
      
 114 
     | 
    
         
            +
                    collection = db["milestone_times"]
         
     | 
| 
      
 115 
     | 
    
         
            +
                  else
         
     | 
| 
      
 116 
     | 
    
         
            +
                    collection = db["lead_times"]
         
     | 
| 
      
 117 
     | 
    
         
            +
                  end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  results = collection.map_reduce(map_function, reduce_function, map_reduce_options)
         
     | 
| 
      
 120 
     | 
    
         
            +
                  
         
     | 
| 
      
 121 
     | 
    
         
            +
                  results.find().map do |result|
         
     | 
| 
      
 122 
     | 
    
         
            +
                    [result['_id'], result["value"]["avg"]]
         
     | 
| 
      
 123 
     | 
    
         
            +
                  end.sort {|a, b| datestring_to_int(a[0]) <=> datestring_to_int(b[0])}.to_json
         
     | 
| 
      
 124 
     | 
    
         
            +
                end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                def datestring_to_int(datestring)
         
     | 
| 
      
 127 
     | 
    
         
            +
                  datestring.split("-").map {|part| part.length == 1 ? "0" + part : part}.join("").to_i
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
              end
         
     | 
| 
       63 
130 
     | 
    
         
             
            end
         
     | 
    
        data/lib/asanban/version.rb
    CHANGED
    
    
    
        data/spec/service_spec.rb
    CHANGED
    
    | 
         @@ -1,6 +1,5 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ENV['RACK_ENV'] = 'test'
         
     | 
| 
       2 
     | 
    
         
            -
            require 'asanban 
     | 
| 
       3 
     | 
    
         
            -
            require 'asanban/bulk_load'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'asanban'
         
     | 
| 
       4 
3 
     | 
    
         
             
            require 'rspec'
         
     | 
| 
       5 
4 
     | 
    
         
             
            require 'rack/test'
         
     | 
| 
       6 
5 
     | 
    
         | 
| 
         @@ -8,83 +7,176 @@ describe Asanban::Service do 
     | 
|
| 
       8 
7 
     | 
    
         
             
              include Rack::Test::Methods
         
     | 
| 
       9 
8 
     | 
    
         | 
| 
       10 
9 
     | 
    
         
             
              before do
         
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
      
 10 
     | 
    
         
            +
                config = { "mongodb_uri" => "mongodb://localhost/", "mongodb_dbname" => "asana_test" }
         
     | 
| 
      
 11 
     | 
    
         
            +
                app.settings.set(:config, config)
         
     | 
| 
      
 12 
     | 
    
         
            +
                conn = Mongo::Connection.from_uri(config['mongodb_uri'])
         
     | 
| 
      
 13 
     | 
    
         
            +
                db = conn.db(config["mongodb_dbname"])
         
     | 
| 
      
 14 
     | 
    
         
            +
                @milestone_times_collection = db["milestone_times"]
         
     | 
| 
      
 15 
     | 
    
         
            +
                @milestone_times_collection.drop
         
     | 
| 
      
 16 
     | 
    
         
            +
                @lead_times_collection = db["lead_times"]
         
     | 
| 
      
 17 
     | 
    
         
            +
                @lead_times_collection.drop
         
     | 
| 
      
 18 
     | 
    
         
            +
                @story_id_counter = 1
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                class Time
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @stub_time = Time.parse("2013-9-30")
         
     | 
| 
      
 22 
     | 
    
         
            +
                  def self.now()
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @stub_time
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
       19 
26 
     | 
    
         
             
              end
         
     | 
| 
       20 
27 
     | 
    
         | 
| 
       21 
28 
     | 
    
         
             
              it "returns average lead times by year" do
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
      
 29 
     | 
    
         
            +
                record_lead_time('Foo', "October 2, 2012", "October 14, 2012") #12 days
         
     | 
| 
      
 30 
     | 
    
         
            +
                record_lead_time('Bar', "May 12, 2013", "May 14, 2013") #2 days
         
     | 
| 
      
 31 
     | 
    
         
            +
                record_lead_time('Baz', "October 12, 2013", "October 16, 2013") #4 days
         
     | 
| 
       25 
32 
     | 
    
         | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
      
 33 
     | 
    
         
            +
                get '/metrics', :aggregate_by => 'year'
         
     | 
| 
      
 34 
     | 
    
         
            +
                #Average: 12 days 2012, 3 days 2013
         
     | 
| 
      
 35 
     | 
    
         
            +
                expect(last_response.body).to eq('[["2012",12.0],["2013",3.0]]')
         
     | 
| 
       29 
36 
     | 
    
         
             
              end
         
     | 
| 
       30 
37 
     | 
    
         | 
| 
       31 
38 
     | 
    
         
             
              it "returns average lead times by month" do
         
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
      
 39 
     | 
    
         
            +
                record_lead_time('Foo', "September 20, 2013", "September 30, 2013") #10 days
         
     | 
| 
      
 40 
     | 
    
         
            +
                record_lead_time('Bar', "October 2, 2013", "October 14, 2013") #12 days
         
     | 
| 
      
 41 
     | 
    
         
            +
                record_lead_time('Baz', "October 12, 2013", "October 16, 2013") #4 days
         
     | 
| 
       35 
42 
     | 
    
         | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
      
 43 
     | 
    
         
            +
                get '/metrics', :aggregate_by => 'month'
         
     | 
| 
      
 44 
     | 
    
         
            +
                #Average: 10 days September, 8 days in October
         
     | 
| 
      
 45 
     | 
    
         
            +
                expect(last_response.body).to eq('[["2013-9",10.0],["2013-10",8.0]]')
         
     | 
| 
       39 
46 
     | 
    
         
             
              end
         
     | 
| 
       40 
47 
     | 
    
         | 
| 
       41 
48 
     | 
    
         
             
              it "returns average lead times by day" do
         
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
      
 49 
     | 
    
         
            +
                record_lead_time('Foo', "October 2, 2013", "October 14, 2013") #12 days
         
     | 
| 
      
 50 
     | 
    
         
            +
                record_lead_time('Bar', "October 12, 2013", "October 14, 2013") #2 days
         
     | 
| 
      
 51 
     | 
    
         
            +
                record_lead_time('Baz', "October 12, 2013", "October 16, 2013") #4 days
         
     | 
| 
       45 
52 
     | 
    
         | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
      
 53 
     | 
    
         
            +
                get '/metrics', :aggregate_by => 'day'
         
     | 
| 
      
 54 
     | 
    
         
            +
                #Average: 7 days 10/14, 4 days 10/16
         
     | 
| 
      
 55 
     | 
    
         
            +
                expect(last_response.body).to eq('[["2013-10-14",7.0],["2013-10-16",4.0]]')
         
     | 
| 
       49 
56 
     | 
    
         
             
              end
         
     | 
| 
       50 
57 
     | 
    
         | 
| 
       51 
58 
     | 
    
         
             
              describe "milestone times recorded" do
         
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
             
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
             
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
             
     | 
| 
       68 
     | 
    
         
            -
             
     | 
| 
       69 
     | 
    
         
            -
             
     | 
| 
       70 
     | 
    
         
            -
             
     | 
| 
       71 
     | 
    
         
            -
             
     | 
| 
       72 
     | 
    
         
            -
             
     | 
| 
       73 
     | 
    
         
            -
             
     | 
| 
      
 59 
     | 
    
         
            +
                before do
         
     | 
| 
      
 60 
     | 
    
         
            +
                  record_time('Foo', false, false, "September 1, 2013", "September 3, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection) #2
         
     | 
| 
      
 61 
     | 
    
         
            +
                  record_time('Foo', false, false, "September 3, 2013", "September 10, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection) #7
         
     | 
| 
      
 62 
     | 
    
         
            +
                  record_time('Foo', false, false, "September 10, 2013", "September 15, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection) #5
         
     | 
| 
      
 63 
     | 
    
         
            +
                  record_time('Bar', false, false, "September 1, 2013", "September 7, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection) #6
         
     | 
| 
      
 64 
     | 
    
         
            +
                  record_time('Bar', false, false, "September 7, 2013", "September 12, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection) #5
         
     | 
| 
      
 65 
     | 
    
         
            +
                  record_time('Bar', false, false, "September 12, 2013", "September 15, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection) #3
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                it "returns average time in 'Dev Ready' by month" do
         
     | 
| 
      
 69 
     | 
    
         
            +
                  get '/metrics', :aggregate_by => 'month', :milestone => 'Dev Ready'
         
     | 
| 
      
 70 
     | 
    
         
            +
                  expect(last_response.body).to eq('[["2013-9",4.0]]')
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                it "returns average time in 'Dev In Progress' by month" do
         
     | 
| 
      
 74 
     | 
    
         
            +
                  get '/metrics', :aggregate_by => 'month', :milestone => 'Dev In Progress'
         
     | 
| 
      
 75 
     | 
    
         
            +
                  expect(last_response.body).to eq('[["2013-9",6.0]]')
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                describe "cycle times" do
         
     | 
| 
      
 79 
     | 
    
         
            +
                  it "returns number of items currently in each phase" do
         
     | 
| 
      
 80 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 83 
     | 
    
         
            +
                    expect(json["PM Test, in Dev"]["current"]).to eq(2.0)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  it "filters out tasks that are completed" do
         
     | 
| 
      
 87 
     | 
    
         
            +
                    record_time('Baz', true, false, "September 1, 2013", "September 8, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection)
         
     | 
| 
      
 88 
     | 
    
         
            +
                    record_time('Baz', true, false, "September 8, 2013", "September 14, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    record_time('Baz', true, false, "September 14, 2013", "September 19, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection)
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 94 
     | 
    
         
            +
                    expect(json["PM Test, in Dev"]["current"]).to eq(2.0)
         
     | 
| 
      
 95 
     | 
    
         
            +
                  end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                  it "filters out deleted tasks" do
         
     | 
| 
      
 98 
     | 
    
         
            +
                    record_time('Baz', false, true, "September 1, 2013", "September 8, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection)
         
     | 
| 
      
 99 
     | 
    
         
            +
                    record_time('Baz', false, true, "September 8, 2013", "September 14, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    record_time('Baz', false, true, "September 14, 2013", "September 19, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection)
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 105 
     | 
    
         
            +
                    expect(json["PM Test, in Dev"]["current"]).to eq(2.0)
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                  it "returns time in phase for items still in phase" do
         
     | 
| 
      
 109 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 1, 2013", "September 8, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection)
         
     | 
| 
      
 110 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 8, 2013", "September 14, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 14, 2013", "September 19, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection)
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 116 
     | 
    
         
            +
                    json["PM Test, in Dev"]["current_days_average"].should be_within(0.01).of(13.66)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  it "returns standard deviation of time in phase for items still in phase" do
         
     | 
| 
      
 120 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 1, 2013", "September 8, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection)
         
     | 
| 
      
 121 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 8, 2013", "September 14, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection)
         
     | 
| 
      
 122 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 14, 2013", "September 19, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection)
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 127 
     | 
    
         
            +
                    json["PM Test, in Dev"]["current_days_stdev"].should be_within(0.01).of(1.88)
         
     | 
| 
      
 128 
     | 
    
         
            +
                  end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                  it "filters out phases with no current items" do
         
     | 
| 
      
 131 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone', :current_milestones_only => true
         
     | 
| 
      
 132 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 133 
     | 
    
         
            +
                    json["Dev Ready"].should be_nil
         
     | 
| 
      
 134 
     | 
    
         
            +
                  end
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                  it "returns number of items that flowed through each phase" do
         
     | 
| 
      
 137 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 138 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 139 
     | 
    
         
            +
                    expect(json["Dev Ready"]["count"]).to eq(2.0)
         
     | 
| 
      
 140 
     | 
    
         
            +
                  end
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                  it "returns cycle time for each phase" do
         
     | 
| 
      
 143 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone'
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 146 
     | 
    
         
            +
                    expect(json["Dev Ready"]["cycle_time"]).to eq(4.0)
         
     | 
| 
      
 147 
     | 
    
         
            +
                  end
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                  it "filters by start and end date" do
         
     | 
| 
      
 150 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 1, 2013", "September 8, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection)
         
     | 
| 
      
 151 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 8, 2013", "September 14, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection)
         
     | 
| 
      
 152 
     | 
    
         
            +
                    record_time('Baz', false, false, "September 14, 2013", "September 19, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection)
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                    get '/metrics', :aggregate_by => 'start_milestone', :start_date => '2013-9-12', :end_date => '2013-9-17'
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                    json = JSON(last_response.body)
         
     | 
| 
      
 157 
     | 
    
         
            +
                    expect(json["PM Test, in Dev"]["current"]).to eq(2.0)
         
     | 
| 
      
 158 
     | 
    
         
            +
                    #TODO: check cycle time
         
     | 
| 
      
 159 
     | 
    
         
            +
                  end
         
     | 
| 
      
 160 
     | 
    
         
            +
                end
         
     | 
| 
      
 161 
     | 
    
         
            +
              end
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
              it "returns an error when an invalid aggregation is specified" do
         
     | 
| 
      
 164 
     | 
    
         
            +
                get '/metrics', :aggregate_by => 'engineer'
         
     | 
| 
       74 
165 
     | 
    
         
             
                expect(last_response.status).to eq(400)
         
     | 
| 
       75 
     | 
    
         
            -
             
     | 
| 
      
 166 
     | 
    
         
            +
              end
         
     | 
| 
       76 
167 
     | 
    
         | 
| 
       77 
168 
     | 
    
         
             
              def app
         
     | 
| 
       78 
169 
     | 
    
         
             
                Asanban::Service
         
     | 
| 
       79 
170 
     | 
    
         
             
              end
         
     | 
| 
       80 
171 
     | 
    
         | 
| 
       81 
172 
     | 
    
         
             
              def record_lead_time(task_id, started_at, ended_at)
         
     | 
| 
       82 
     | 
    
         
            -
             
     | 
| 
      
 173 
     | 
    
         
            +
                record_time(task_id, false, false, started_at, ended_at, 'Dev Ready', 'Production', @lead_times_collection)
         
     | 
| 
       83 
174 
     | 
    
         
             
              end
         
     | 
| 
       84 
175 
     | 
    
         | 
| 
       85 
     | 
    
         
            -
              def record_time(task_id, started_at, ended_at, start_milestone, end_milestone, collection)
         
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
             
     | 
| 
       88 
     | 
    
         
            -
             
     | 
| 
      
 176 
     | 
    
         
            +
              def record_time(task_id, completed, deleted, started_at, ended_at, start_milestone, end_milestone, collection)
         
     | 
| 
      
 177 
     | 
    
         
            +
                start_story = {'id' => @story_id_counter, 'created_at' => started_at}
         
     | 
| 
      
 178 
     | 
    
         
            +
                @story_id_counter += 1
         
     | 
| 
      
 179 
     | 
    
         
            +
                end_story = {'id' => @story_id_counter, 'created_at' => ended_at}
         
     | 
| 
      
 180 
     | 
    
         
            +
                Asanban::BulkLoad.record_time(task_id, completed, deleted, start_story, start_milestone, end_story, end_milestone, collection)
         
     | 
| 
       89 
181 
     | 
    
         
             
              end
         
     | 
| 
       90 
182 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            <html ng-app="asanbanApp">
         
     | 
| 
      
 2 
     | 
    
         
            +
            	<head>
         
     | 
| 
      
 3 
     | 
    
         
            +
            		<link href="https://public.bizo.com/bizstrap/css/bizstrap-v2.2.2.15.css" rel="stylesheet" />
         
     | 
| 
      
 4 
     | 
    
         
            +
            		<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.6/angular.min.js"></script>
         
     | 
| 
      
 5 
     | 
    
         
            +
            		<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.6/angular-resource.min.js"></script>
         
     | 
| 
      
 6 
     | 
    
         
            +
            		<script src="js/controllers.js"></script>
         
     | 
| 
      
 7 
     | 
    
         
            +
            	</head>
         
     | 
| 
      
 8 
     | 
    
         
            +
            	<body ng-controller="AsanbanDashboardCtrl">
         
     | 
| 
      
 9 
     | 
    
         
            +
            		<h1 class="section-header">Kanban Dashboard</h1>
         
     | 
| 
      
 10 
     | 
    
         
            +
            		<div class="control-group">
         
     | 
| 
      
 11 
     | 
    
         
            +
            			<label class="control-label">Analysis Begin Date</label>
         
     | 
| 
      
 12 
     | 
    
         
            +
            			<div class="controls">
         
     | 
| 
      
 13 
     | 
    
         
            +
            				<input type="text" class="input-xlarge" ng-model="start_date" />
         
     | 
| 
      
 14 
     | 
    
         
            +
            			</div>
         
     | 
| 
      
 15 
     | 
    
         
            +
            		</div>
         
     | 
| 
      
 16 
     | 
    
         
            +
            		<div class="control-group">
         
     | 
| 
      
 17 
     | 
    
         
            +
            			<label class="control-label">Analysis End Date</label>
         
     | 
| 
      
 18 
     | 
    
         
            +
            			<div class="controls">
         
     | 
| 
      
 19 
     | 
    
         
            +
            				<input type="text" class="input-xlarge" ng-model="end_date" />
         
     | 
| 
      
 20 
     | 
    
         
            +
            			</div>
         
     | 
| 
      
 21 
     | 
    
         
            +
            		</div>
         
     | 
| 
      
 22 
     | 
    
         
            +
            		<button class="btn btn-large" ng-click="query()">Filter</button>
         
     | 
| 
      
 23 
     | 
    
         
            +
            		<table class="table">
         
     | 
| 
      
 24 
     | 
    
         
            +
            			<thead>
         
     | 
| 
      
 25 
     | 
    
         
            +
            				<tr>
         
     | 
| 
      
 26 
     | 
    
         
            +
            					<th>Phase</th>
         
     | 
| 
      
 27 
     | 
    
         
            +
            					<th>Total # of Items</th>
         
     | 
| 
      
 28 
     | 
    
         
            +
            					<th>Average # of Days in State</th>
         
     | 
| 
      
 29 
     | 
    
         
            +
            					<th>Std Deviation of Days in State</th>
         
     | 
| 
      
 30 
     | 
    
         
            +
            					<th># of Items that Flowed Through in Date Range</th>
         
     | 
| 
      
 31 
     | 
    
         
            +
            				</tr>
         
     | 
| 
      
 32 
     | 
    
         
            +
            			</thead>
         
     | 
| 
      
 33 
     | 
    
         
            +
            			<tbody>
         
     | 
| 
      
 34 
     | 
    
         
            +
            				<tr ng-repeat="(phase, phase_metrics) in metrics">
         
     | 
| 
      
 35 
     | 
    
         
            +
            					<td>{{phase}}</td>
         
     | 
| 
      
 36 
     | 
    
         
            +
            					<td>{{phase_metrics.current | number}}</td>
         
     | 
| 
      
 37 
     | 
    
         
            +
            					<td>{{phase_metrics.current_days_average | number:0}}</td>
         
     | 
| 
      
 38 
     | 
    
         
            +
            					<td>{{phase_metrics.current_days_stdev | number:1}}</td>
         
     | 
| 
      
 39 
     | 
    
         
            +
            					<td>{{phase_metrics.count | number}}</td>
         
     | 
| 
      
 40 
     | 
    
         
            +
            				</tr>
         
     | 
| 
      
 41 
     | 
    
         
            +
            			</tbody>
         
     | 
| 
      
 42 
     | 
    
         
            +
            		</table>
         
     | 
| 
      
 43 
     | 
    
         
            +
            	</body>
         
     | 
| 
      
 44 
     | 
    
         
            +
            </html>
         
     | 
| 
         @@ -0,0 +1,28 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            var asanbanApp = angular.module('asanbanApp', ['ngResource']);
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            //TODO: Move this into a separate file
         
     | 
| 
      
 4 
     | 
    
         
            +
            asanbanApp.factory('Metrics', ['$resource',
         
     | 
| 
      
 5 
     | 
    
         
            +
              function($resource){
         
     | 
| 
      
 6 
     | 
    
         
            +
                return $resource('/metrics?aggregate_by=start_milestone¤t_milestones_only=true&start_date=:start_date&end_date=:end_date', 
         
     | 
| 
      
 7 
     | 
    
         
            +
                	{}, {
         
     | 
| 
      
 8 
     | 
    
         
            +
                  query: {method:'GET'}
         
     | 
| 
      
 9 
     | 
    
         
            +
                });
         
     | 
| 
      
 10 
     | 
    
         
            +
              }]);
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            asanbanApp.controller('AsanbanDashboardCtrl', ['$scope', 'Metrics',
         
     | 
| 
      
 13 
     | 
    
         
            +
              function($scope, Metrics) {
         
     | 
| 
      
 14 
     | 
    
         
            +
              	var format_date = function(date) {
         
     | 
| 
      
 15 
     | 
    
         
            +
              		return date.toISOString().substring(0, 10);
         
     | 
| 
      
 16 
     | 
    
         
            +
              	};
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              	var start_date = new Date();
         
     | 
| 
      
 19 
     | 
    
         
            +
            		start_date.setMonth(start_date.getMonth() - 2);
         
     | 
| 
      
 20 
     | 
    
         
            +
              	$scope.start_date = format_date(start_date);
         
     | 
| 
      
 21 
     | 
    
         
            +
              	$scope.end_date = format_date(new Date());
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                $scope.query = function() {
         
     | 
| 
      
 24 
     | 
    
         
            +
            	    $scope.metrics = Metrics.query({start_date: $scope.start_date, end_date: $scope.end_date});
         
     | 
| 
      
 25 
     | 
    
         
            +
                };
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                $scope.query();
         
     | 
| 
      
 28 
     | 
    
         
            +
              }]);
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: asanban
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0.0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.0.5
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Pat Gannon
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire: 
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date:  
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2014-04-30 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: mongo
         
     | 
| 
         @@ -66,6 +66,20 @@ dependencies: 
     | 
|
| 
       66 
66 
     | 
    
         
             
                - - ! '>='
         
     | 
| 
       67 
67 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       68 
68 
     | 
    
         
             
                    version: '0'
         
     | 
| 
      
 69 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 70 
     | 
    
         
            +
              name: descriptive_statistics
         
     | 
| 
      
 71 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 72 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 73 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 74 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 75 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 76 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 77 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 78 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 79 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 80 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 81 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 82 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
       69 
83 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       70 
84 
     | 
    
         
             
              name: rspec
         
     | 
| 
       71 
85 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -116,6 +130,8 @@ files: 
     | 
|
| 
       116 
130 
     | 
    
         
             
            - lib/asanban/service.rb
         
     | 
| 
       117 
131 
     | 
    
         
             
            - lib/asanban/version.rb
         
     | 
| 
       118 
132 
     | 
    
         
             
            - spec/service_spec.rb
         
     | 
| 
      
 133 
     | 
    
         
            +
            - static/dashboard.html
         
     | 
| 
      
 134 
     | 
    
         
            +
            - static/js/controllers.js
         
     | 
| 
       119 
135 
     | 
    
         
             
            - static/metrics.html
         
     | 
| 
       120 
136 
     | 
    
         
             
            homepage: ''
         
     | 
| 
       121 
137 
     | 
    
         
             
            licenses: []
         
     |