asana_snapshot 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 97597233f2dbc17a8f86f9cfc588a979b308e37b
4
+ data.tar.gz: 4e0e25b7f25414548ae8bb5f1b74dbcbe2b38bc6
5
+ SHA512:
6
+ metadata.gz: 6e53bae9059fb06962becb66404d8f895c5e414f8bb873395558fac80206fadb7d06db3175cc996e8ebc957122c54aa1010d3aeafb6bf2d7bfcb71959d57af13
7
+ data.tar.gz: 88eed4add936e327f4a7cb47448996ebe3bfc095693e91a2ac3b2d7538d0bb528c443f1eae079ba971081ed8cce530c7746f3b7ef23605f04d67266c66e0b1ed
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2018 Matt Yeh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ ## Asana Snapshot
2
+
3
+ [Asana](https://www.asana.com) is a great, versatile task management system. However, like many similar task management systems, being able to track meaningful progress across one or more projects; burn-down charts are great, but are not built to extract any immediate, actionable decisions. Tracking the progress of a project over a specified period of time can be quite cumbersome, if not impossible. This becomes even more difficult in Asana when using Boards that might all have different names for the swim lanes/columns or that have different terminology for what tasks are 'complete' and 'incomplete'.
4
+
5
+ Asana Snapshot has chosen to solve this issue by leveraging a change-tracking tool that is likely a part of most of our daily lives: [git](https://git-scm.com/). Using git comparisons, one can gather information to drive useful & productive conversations about the progress of a given project, such as, overall velocity, project completeness and burndown, which specific tasks were completed in that time period, which specific tasks are at-risk (eg, which ones are assigned, but not completed or which remain unassigned within that given time period), etc.
6
+
7
+ From a technical side, this gem will fetch all tasks based on a specific set of user-provided conditions, write to a text-based project snapshot file (ie, using Markdown), commit & tag those project snapshot files into a git repository, while doing so in a simple and repeatable manner.
8
+
9
+ ### Sample snapshot
10
+
11
+ ```
12
+ ## Stats
13
+ Complete: 12
14
+ Incomplete: 23 (12 unassigned)
15
+
16
+ ## Tasks
17
+ [X] 473716394 ["Done"] - Joe Smith - provision test server
18
+ [ ] 473716485 ["In Progress"] - John Doe - implement authentication service
19
+ [ ] 473164858 ["Ready To Dev"] - Unassigned - develop profitable feature
20
+ ```
21
+
22
+ Tasks are ordered by Asana task id, and so should almost always remain on the same line within a snapshot file, making tasks updates easier to parse out from the git tag comparisons.
23
+
24
+ ### Installation
25
+
26
+ ```
27
+ gem install asana_snapshot
28
+ ```
29
+
30
+ or if you are using Bundler add the following to your Gemfile
31
+
32
+ ```
33
+ gem 'asana_snapshot'
34
+ ```
35
+
36
+ ### Configuration
37
+
38
+ | Config | Description | Default |
39
+ | ------------- | --------------------------------------------------------------------- | -------------------:|
40
+ | logger | gem logging destination | Logger.new STDOUT |
41
+ | token | Asana API Personal Access Token | nil |
42
+ | base_dir | file directory where snapshots will be saved | Dir.pwd |
43
+ | persistence | Hash specifying the persistence store (requires an `adapter` key) | {adapter: :git} |
44
+
45
+ Example:
46
+ ```
47
+ AsanaSnapshot.configure do |config|
48
+ config.logger = Logger.new('logfile.log')
49
+ config.token = '_my_personal_token_'
50
+ config.base_dir = '/path/to/where/i/save/snapshots'
51
+ end
52
+ ```
53
+
54
+ ### Usage
55
+
56
+ There are two primary entry points for executing AsanaSnapshot: via its Ruby API or via the shipped executable file.
57
+
58
+ In both cases, it expects the presence of a YAML config file which supplies information about the Asana workspace & projects upon which it will search for tasks. AsanaSnapshot will only search for tasks (as opposed to sub-tasks) that belong to the projects in the YAML file and which contain any tags defined in the YAML file.
59
+
60
+ #### Anatomy of a snapshot YAML file
61
+
62
+ ```
63
+ title: 'Acme Boards'
64
+ ```
65
+ The `title` key identifies how the snapshots will be organized. The sub-directory of the configured `base_dir` in which snapshots are saved will be an underscored version of this `title`.
66
+
67
+ ```
68
+ workspace: 123456789
69
+ ```
70
+ The `workspace` key identifies which Asana workspace id to query for tasks.
71
+
72
+ ```
73
+ filters:
74
+ tags: 234567890, 345678901
75
+ ```
76
+ The `filters` key groups the possible query filters. As of now, only `tags` are supported. `tags` are a comma-delimited list of Asana tag id's that a task may contain.
77
+
78
+ ```
79
+ projects:
80
+ - id: 4567890123
81
+ name: 'Phase 2 Widgets'
82
+ columns:
83
+ complete:
84
+ - 'Done'
85
+ incomplete:
86
+ - 'In Discovery'
87
+ - 'In Progress'
88
+ ```
89
+ The `projects` key identifies the set of projects that will be searched. It contains the Asana project id, a helpful `name` to identify it in the YAML file (which would likely match the name of the project in Asana, but it can be anything you want), and a `columns` key which identifies the names of the Asana sections of the project board should be considered as `complete` or `incomplete`.
90
+
91
+ #### Ruby API
92
+
93
+ Example:
94
+ ```
95
+ AsanaSnapshot.configure do |config|
96
+ config.token = '_my_personal_token_'
97
+ end
98
+
99
+ AsanaSnapshot.execute './config/acme_boards.yml'
100
+ ```
101
+
102
+ #### Executable
103
+
104
+ The gem ships with an executable named `snap`. `snap` takes a single argument, which is the path to the `asana_snapshot` config file.
105
+
106
+ The `snap` executable uses the following environment variables to override the default AsanaSnapshot configuration:
107
+
108
+ | ENV | Config |
109
+ | ----------------------- |:-------:|
110
+ | ASANA_SNAPSHOT_TOKEN | token |
111
+
112
+ Example:
113
+ ```
114
+ ASANA_SNAPSHOT_TOKEN='_my_personal_token_' snap './config/acme_boards.yml'
115
+ ```
data/bin/snap ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'asana_snapshot'
4
+
5
+ AsanaSnapshot.configure do |config|
6
+ config.token = ENV['ASANA_SNAPSHOT_TOKEN'] if ENV['ASANA_SNAPSHOT_TOKEN']
7
+ end
8
+
9
+ AsanaSnapshot.execute ARGV[0]
@@ -0,0 +1,14 @@
1
+ require 'logger'
2
+
3
+ class AsanaSnapshot::Configuration
4
+ attr_accessor :logger, :token, :base_dir, :persistence
5
+
6
+ def initialize
7
+ @logger = Logger.new STDOUT
8
+ @token = nil
9
+ @base_dir = Dir.pwd
10
+ @persistence = {
11
+ adapter: :git
12
+ }
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ require 'git'
2
+
3
+ class AsanaSnapshot::Persistence::GitAdapter
4
+ def initialize
5
+ @repo = Git.init(AsanaSnapshot.configuration.base_dir, log: AsanaSnapshot.configuration.logger)
6
+ end
7
+
8
+ def mark_for_save(file)
9
+ @repo.add file
10
+ true
11
+ end
12
+
13
+ def save(group)
14
+ today = Time.now.strftime('%Y-%m-%d')
15
+ @repo.commit "[#{group}] Snapshot: #{today}"
16
+
17
+ if @repo.tags.include?(today)
18
+ @repo.delete_tag today
19
+ end
20
+ @repo.add_tag today
21
+ true
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ class AsanaSnapshot::Persistence
2
+ extend Forwardable
3
+
4
+ attr_reader :adapter
5
+
6
+ def_delegators :adapter, :mark_for_save, :save
7
+
8
+ def initialize(adapter: :git)
9
+ case adapter.to_sym
10
+ when :git
11
+ require 'asana_snapshot/persistence/git'
12
+ @adapter = AsanaSnapshot::Persistence::GitAdapter.new
13
+ else
14
+ raise ArgumentError, "Unknown persistence adapter: #{adapter}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ require 'fileutils'
2
+
3
+ module AsanaSnapshot
4
+ class SnapshotGenerator
5
+ attr_reader :snapshot_directory, :file, :project_id, :project
6
+
7
+ def initialize(tasks, group:, project_id:)
8
+ @project_id = project_id
9
+ @project = compile tasks
10
+
11
+ directory_name = group.downcase.gsub(' ', '_')
12
+ @snapshot_directory = "#{AsanaSnapshot.configuration.base_dir}/snapshots/#{directory_name}"
13
+
14
+ file_name = project[:name].gsub(' ', '_').gsub('&', 'and')
15
+ @file = "#{snapshot_directory}/#{file_name}.md"
16
+ end
17
+
18
+ def write
19
+ setup_directory
20
+
21
+ File.open(file, "w") do |f|
22
+ f.puts '## Stats'
23
+ f.puts "Complete: #{project[:stats][:complete]}"
24
+ f.puts "Incomplete: #{project[:stats][:incomplete]} (#{project[:stats][:unassigned]} unassigned)"
25
+ f.puts ''
26
+
27
+ f.puts '## Tasks'
28
+ project[:tasks].each do |task|
29
+ f.puts "[#{task.completed? ? 'X' : ' '}] #{task}"
30
+ end
31
+ end
32
+
33
+ AsanaSnapshot.persistence_store.mark_for_save file
34
+ end
35
+
36
+ private
37
+
38
+ def setup_directory
39
+ return if ::File.directory? snapshot_directory
40
+ ::FileUtils.mkdir_p(snapshot_directory)
41
+ ::FileUtils.chmod 0755, snapshot_directory
42
+ end
43
+
44
+ def compile(tasks)
45
+ tasks.sort_by(&:id).reduce({
46
+ name: nil,
47
+ tasks: [],
48
+ stats: {
49
+ complete: 0,
50
+ incomplete: 0,
51
+ unassigned: 0
52
+ }
53
+ }) do |memo, task|
54
+ task.project_columns.reject do |project_column|
55
+ project_column.project_id.to_i != project_id.to_i
56
+ end.each do |project_column|
57
+ memo[:name] ||= project_column.project_name
58
+ memo[:tasks].push task
59
+ memo[:stats][:complete]+=1 if task.completed?
60
+ memo[:stats][:incomplete]+=1 unless task.completed?
61
+ memo[:stats][:unassigned]+=1 if task.assignee_name.nil?
62
+ end
63
+ memo
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,46 @@
1
+ module AsanaSnapshot
2
+ class Task
3
+ extend Forwardable
4
+
5
+ def_delegators :@task, :id, :name
6
+
7
+ def initialize(task_response)
8
+ @task = OpenStruct.new(task_response)
9
+ end
10
+
11
+ def assignee_name
12
+ @task.assignee&.send(:[], 'name')
13
+ end
14
+
15
+ def completed?
16
+ completed_task? || completed_column?
17
+ end
18
+
19
+ def project_columns
20
+ @project_columns ||= @task.memberships.map do |m|
21
+ OpenStruct.new(
22
+ project_id: m['project']['id'],
23
+ project_name: m['project']['name'],
24
+ column_name: m['section']['name']
25
+ )
26
+ end
27
+ end
28
+
29
+ def to_s
30
+ "#{id} #{project_columns.map(&:column_name)} - #{assignee_name || 'Unassigned'} - #{name}"
31
+ end
32
+
33
+ def completed_task?
34
+ @task.completed
35
+ end
36
+
37
+ def completed_column?
38
+ project_columns.map do |project_column|
39
+ project_config = AsanaSnapshot.projects.detect do |project|
40
+ project['id'] == project_column.project_id
41
+ end
42
+ project_config && project_config['columns']['complete']&.include?(project_column.column_name)
43
+ end.any?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ require 'asana'
2
+ require_relative './task'
3
+
4
+ module AsanaSnapshot
5
+ class TaskSearcher
6
+ include Asana::Resources::ResponseHelper
7
+ extend Asana::Resources::ResponseHelper
8
+
9
+ attr_reader :client, :workspace_id
10
+
11
+ TASK_FIELDS = {
12
+ fields: [
13
+ 'completed',
14
+ 'name',
15
+ 'assignee.name',
16
+ 'memberships.(project|section).name'
17
+ ]
18
+ }.freeze
19
+
20
+ def initialize(token: required('token'), workspace_id: required('workspace_id'))
21
+ @client = Asana::Client.new do |c|
22
+ c.authentication :access_token, token
23
+ end
24
+ @workspace_id = workspace_id
25
+ end
26
+
27
+ def search(search_options = {})
28
+ endpoint = "/workspaces/#{workspace_id}/tasks/search"
29
+ search_results = parse client.get(endpoint, params: search_options, options: TASK_FIELDS)
30
+ search_results.first.map do |search_result|
31
+ AsanaSnapshot::Task.new search_result
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+
4
+ require 'asana_snapshot/task_searcher'
5
+ require 'asana_snapshot/snapshot_generator'
6
+ require 'asana_snapshot/configuration'
7
+ require 'asana_snapshot/persistence'
8
+
9
+ module AsanaSnapshot
10
+ class << self
11
+ attr_accessor :projects
12
+ attr_writer :configuration, :persistence_store
13
+ end
14
+
15
+ def self.configuration
16
+ @configuration ||= AsanaSnapshot::Configuration.new
17
+ end
18
+
19
+ def self.persistence_store
20
+ @persistence_store ||= AsanaSnapshot::Persistence.new(adapter: AsanaSnapshot.configuration.persistence[:adapter])
21
+ end
22
+
23
+ def self.configure
24
+ yield configuration
25
+ end
26
+
27
+ def self.execute(config_file)
28
+ unless self.configuration.token
29
+ self.configuration.logger.error "No Asana token configured."
30
+ else
31
+ config = YAML.load_file config_file
32
+
33
+ self.projects = config['projects']
34
+ self.projects.each do |project|
35
+ tasks = AsanaSnapshot::TaskSearcher.new(
36
+ token: self.configuration.token,
37
+ workspace_id: config['workspace']
38
+ ).search(
39
+ 'tags.any' => config['filters']['tags'],
40
+ 'projects.any' => project['id'],
41
+ 'is_subtask' => false
42
+ )
43
+
44
+ if tasks.any?
45
+ AsanaSnapshot::SnapshotGenerator.new(
46
+ tasks,
47
+ group: config['title'],
48
+ project_id: project['id']
49
+ ).write
50
+ self.configuration.logger.info "Successfully created snapshot for #{project['name']}"
51
+ else
52
+ self.configuration.logger.info "No tasks found for #{project['name']}"
53
+ end
54
+ end
55
+
56
+ self.persistence_store.save config['title']
57
+ end
58
+ end
59
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asana_snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Matt Yeh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: asana
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: git
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 1.5.0
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.5.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: 1.5.0
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.5.0
53
+ description: " AsanaSnapshot wraps the official Asana ruby client to search for
54
+ tasks, write them to text files, and check them into a git repository.\n"
55
+ email: dev.mtyeh411@gmail.com
56
+ executables:
57
+ - snap
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - LICENSE
62
+ - README.md
63
+ - bin/snap
64
+ - lib/asana_snapshot.rb
65
+ - lib/asana_snapshot/configuration.rb
66
+ - lib/asana_snapshot/persistence.rb
67
+ - lib/asana_snapshot/persistence/git.rb
68
+ - lib/asana_snapshot/snapshot_generator.rb
69
+ - lib/asana_snapshot/task.rb
70
+ - lib/asana_snapshot/task_searcher.rb
71
+ homepage: https://github.com/mtyeh411/asana_snapshot
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.4.2
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.6.14
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Save snapshots of Asana tasks.
95
+ test_files: []