asana 0.0.6 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +9 -9
  2. data/.codeclimate.yml +4 -0
  3. data/.gitignore +12 -20
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +18 -0
  6. data/.travis.yml +12 -0
  7. data/.yardopts +5 -0
  8. data/CODE_OF_CONDUCT.md +13 -0
  9. data/Gemfile +17 -0
  10. data/Guardfile +85 -4
  11. data/LICENSE.txt +21 -0
  12. data/README.md +264 -135
  13. data/Rakefile +62 -7
  14. data/asana.gemspec +27 -21
  15. data/examples/Gemfile +6 -0
  16. data/examples/Gemfile.lock +56 -0
  17. data/examples/api_token.rb +21 -0
  18. data/examples/cli_app.rb +25 -0
  19. data/examples/events.rb +38 -0
  20. data/examples/omniauth_integration.rb +54 -0
  21. data/lib/asana.rb +8 -11
  22. data/lib/asana/authentication.rb +8 -0
  23. data/lib/asana/authentication/oauth2.rb +42 -0
  24. data/lib/asana/authentication/oauth2/access_token_authentication.rb +51 -0
  25. data/lib/asana/authentication/oauth2/bearer_token_authentication.rb +32 -0
  26. data/lib/asana/authentication/oauth2/client.rb +50 -0
  27. data/lib/asana/authentication/token_authentication.rb +20 -0
  28. data/lib/asana/client.rb +124 -0
  29. data/lib/asana/client/configuration.rb +165 -0
  30. data/lib/asana/errors.rb +90 -0
  31. data/lib/asana/http_client.rb +155 -0
  32. data/lib/asana/http_client/environment_info.rb +53 -0
  33. data/lib/asana/http_client/error_handling.rb +103 -0
  34. data/lib/asana/http_client/response.rb +32 -0
  35. data/lib/asana/resources.rb +11 -0
  36. data/lib/asana/resources/attachment.rb +44 -0
  37. data/lib/asana/resources/attachment_uploading.rb +33 -0
  38. data/lib/asana/resources/collection.rb +68 -0
  39. data/lib/asana/resources/event.rb +49 -0
  40. data/lib/asana/resources/event_subscription.rb +12 -0
  41. data/lib/asana/resources/events.rb +101 -0
  42. data/lib/asana/resources/project.rb +145 -19
  43. data/lib/asana/resources/registry.rb +62 -0
  44. data/lib/asana/resources/resource.rb +103 -0
  45. data/lib/asana/resources/response_helper.rb +14 -0
  46. data/lib/asana/resources/story.rb +58 -7
  47. data/lib/asana/resources/tag.rb +111 -19
  48. data/lib/asana/resources/task.rb +284 -57
  49. data/lib/asana/resources/team.rb +55 -0
  50. data/lib/asana/resources/user.rb +65 -10
  51. data/lib/asana/resources/workspace.rb +79 -34
  52. data/lib/asana/ruby2_0_0_compatibility.rb +3 -0
  53. data/lib/asana/version.rb +3 -1
  54. data/lib/templates/index.js +8 -0
  55. data/lib/templates/resource.ejs +225 -0
  56. data/package.json +7 -0
  57. metadata +91 -51
  58. data/LICENSE +0 -22
  59. data/lib/asana/config.rb +0 -23
  60. data/lib/asana/resource.rb +0 -52
  61. data/spec/asana/resources/project_spec.rb +0 -63
  62. data/spec/asana/resources/story_spec.rb +0 -39
  63. data/spec/asana/resources/tag_spec.rb +0 -63
  64. data/spec/asana/resources/task_spec.rb +0 -95
  65. data/spec/asana/resources/user_spec.rb +0 -64
  66. data/spec/asana/resources/workspace_spec.rb +0 -108
  67. data/spec/spec_helper.rb +0 -9
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YTljM2UwZDM5YWU3ZjQ4MWIwZWRhNjRhYzRhNTQyZjcxYzI5ODg0Zg==
4
+ ZjVmMmMzNGZiODUxMDA4ODlkODg3NTBhMDNlZTVjYjdmMWU3YjUxYQ==
5
5
  data.tar.gz: !binary |-
6
- MDNjZGNiZDIyNWE4YzZkYzZkZjk3N2IzODQxZjQ2MmM2ZGRlMWYyYw==
7
- !binary "U0hBNTEy":
6
+ ZTI3MWMwYzg5ZDkyNTIzYmUxYmE0NDZmNDJhM2M3ODFmNzdlNGRlYQ==
7
+ SHA512:
8
8
  metadata.gz: !binary |-
9
- YWEyOTRjYjIzNDc5MjgxZGEyODhjODNkMWJlYTU0MzljYTQxODBiOTNjOGM5
10
- OTg5ZDlkODBlYTc4MjZlZjM4ZmE2NTk3MjUxOTM0MWVmZmJlOThmNWExNWY2
11
- NTQwNzU2NjNiZTlkZjdiMzdmYzk4NjM3YjRmMjQ5ZGYzM2FiZDM=
9
+ NDg1YjlmOGQwMjk5MjJmZDllNTc2NDE0ZTQxNWYwYTZlZTg1NWVjYjg4NTRi
10
+ YTJkMTMyNjYxNjI5NThlMTQ1ZWExZmM2M2MwN2QwYTJmMDQ3NWZiMWE5ZTNl
11
+ ZmQ2OWYzMGM0ZjMwY2M4NDM4YWQ5NDk0NTYyMDIxMjI1M2VmMzQ=
12
12
  data.tar.gz: !binary |-
13
- NTY0ZDYzZTRhOGM0MTNhYjgyY2I4NWQ3MGM5YWNlMDMyYzhmZjg3ZWE0NGNj
14
- ZjE3ZmNhYmNhNGU4MzgyNDY5YjE3NjVhYjdkMzQzOWFlMTlkNzkzOGFjZjE2
15
- ZDYxZTdlNzIxMjE2MjY4MWZiZGE4YTkzMmVkMjc3ZGVlYzAxMmE=
13
+ MmQ1ZTI4ODRjYjk0MWFiNGMxZWM3ZTkyNDk0OWJkNTk5NzJkMDYxYWFiOTdh
14
+ ZDEyOGIxZDU5YzQxMjU0NmNlYjJhZDkzZDBkYmY2NGUxMWMyN2FhOGU2MmQ2
15
+ NzBkYzc1M2EwYWI1MTI5YzhhZmU0MjRiMzY4Nzg1NzVhNjZkZmE=
@@ -0,0 +1,4 @@
1
+ languages:
2
+ Ruby: true
3
+ exclude_paths:
4
+ - "lib/asana/resources/*"
data/.gitignore CHANGED
@@ -1,20 +1,12 @@
1
- *.gem
2
- *.rbc
3
- *.un~
4
- .DS_Store
5
- .bundle
6
- .config
7
- .yardoc
8
- Gemfile.lock
9
- InstalledFiles
10
- _yardoc
11
- coverage
12
- doc/
13
- lib/bundler/man
14
- pkg
15
- rdoc
16
- spec/fixtures/cassettes
17
- spec/reports
18
- test/tmp
19
- test/version_tmp
20
- tmp
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /bin/
11
+ test.rb
12
+ /node_modules/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --require spec_helper
3
+ --require asana
4
+ --order random
@@ -0,0 +1,18 @@
1
+ AllCops:
2
+ Include:
3
+ - '**/Rakefile'
4
+ Exclude:
5
+ - 'bin/**/*'
6
+ - 'examples/**/*'
7
+ - 'lib/asana/resources/attachment.rb'
8
+ - 'lib/asana/resources/project.rb'
9
+ - 'lib/asana/resources/story.rb'
10
+ - 'lib/asana/resources/tag.rb'
11
+ - 'lib/asana/resources/task.rb'
12
+ - 'lib/asana/resources/team.rb'
13
+ - 'lib/asana/resources/user.rb'
14
+ - 'lib/asana/resources/workspace.rb'
15
+ - 'spec/templates/unicorn.rb'
16
+ - 'spec/templates/world.rb'
17
+ - 'test.rb'
18
+ require: rubocop-rspec
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ - 2.0.0
5
+ deploy:
6
+ provider: rubygems
7
+ api_key:
8
+ secure: ZFzExKt6auBOcQyg8955GwlZW2OaS64Q+XHGSay2MzjALw1iiy5uuMdwkueCKrGWSv6/eBe9jjsmYBe3OfkUIpIBbUacnbEYeaC70AyucNjvFrrl0YVYHb7neojarJUmKz9bz9Pkju/jdxksaYaj58xfq5YPQDfjFtdmylvuNEYpujT6goPEbxG4U4PpIhhQOZRDRXXAPS+f7jHejTSK06kvJjiJw0d51VJtBbp+0TKNKL6BDKdOKjKeHuebuUmSw8crDyaYdnwYwmNg1cJrGOv2t76M08zoKkkIO2lwPMHisi1/+cbVcZfxM4SfdHJeU6cQuRdb0uCUbbj6GsGwT8vWP2mGUrLe4UV/GfZDmvK3MKeKIlkgig31a3Qny9yjn8EjSnKHYuHBbJvPQDPPpFUfgEneUxn2t4P6m+epkd1gldWqTWf8mhMR/6xAFT4s+BaxnMMJsTC3Ea+dZZ30EqCw/kx5B2Z1KVLgsxHeMN/Q+AeOcbOvlGDsFL0Mjk/PqDTW1AWKLs/D1ohcxjSmlNJGWR6JHa/Ei0GqjDE2+/ZGsKsRfcDD4kU5qnKdqdzDlbL3cL4tChzuWVcguYdrg1yZzqPrCPzmy+2D7Hphyaj9CPKEh7qwT+IQU5o/V2peOJUjKrMlJS4gFq6MvTDh5U59J88Kkg72DXhcEUcySkU=
9
+ gem: asana
10
+ on:
11
+ tags: true
12
+ repo: Asana/ruby-asana
@@ -0,0 +1,5 @@
1
+ --title "Asana API Ruby Client"
2
+ --readme README.md
3
+ --plugin tomdoc
4
+ lib
5
+ - LICENSE.txt
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile CHANGED
@@ -2,3 +2,20 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in asana.gemspec
4
4
  gemspec
5
+
6
+ group :tools do
7
+ gem 'rubocop'
8
+ gem 'rubocop-rspec'
9
+
10
+ gem 'guard'
11
+ gem 'guard-rspec'
12
+ gem 'guard-rubocop'
13
+ gem 'guard-yard'
14
+
15
+ gem 'yard'
16
+ gem 'yard-tomdoc'
17
+
18
+ gem 'byebug'
19
+
20
+ gem 'simplecov', require: false
21
+ end
data/Guardfile CHANGED
@@ -1,5 +1,86 @@
1
- guard 'minitest' do
2
- watch(%r|^lib/(.*/)?([^/]+)\.rb$|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
3
- watch(%r|^spec/spec_helper\.rb|) { "spec" }
4
- watch(%r|^spec/(.*)\/?(.*)_spec\.rb|)
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features)
6
+
7
+ ## Uncomment to clear the screen before every task
8
+ # clearing :on
9
+
10
+ ## Guard internally checks for changes in the Guardfile and exits.
11
+ ## If you want Guard to automatically start up again, run guard in a
12
+ ## shell loop, e.g.:
13
+ ##
14
+ ## $ while bundle exec guard; do echo "Restarting Guard..."; done
15
+ ##
16
+ ## Note: if you are using the `directories` clause above and you are not
17
+ ## watching the project directory ('.'), then you will want to move
18
+ ## the Guardfile to a watched dir and symlink it back, e.g.
19
+ #
20
+ # $ mkdir config
21
+ # $ mv Guardfile config/
22
+ # $ ln -s config/Guardfile .
23
+ #
24
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
25
+
26
+ # Note: The cmd option is now required due to the increasing number of ways
27
+ # rspec may be run, below are examples of the most common uses.
28
+ # * bundler: 'bundle exec rspec'
29
+ # * bundler binstubs: 'bin/rspec'
30
+ # * spring: 'bin/rspec' (This will use spring if running and you have
31
+ # installed the spring binstubs per the docs)
32
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
33
+ # * 'just' rspec: 'rspec'
34
+
35
+ guard :rspec, cmd: "bundle exec rspec" do
36
+ require "guard/rspec/dsl"
37
+ dsl = Guard::RSpec::Dsl.new(self)
38
+
39
+ # Feel free to open issues for suggestions and improvements
40
+
41
+ # RSpec files
42
+ rspec = dsl.rspec
43
+ watch(rspec.spec_helper) { rspec.spec_dir }
44
+ watch(rspec.spec_support) { rspec.spec_dir }
45
+ watch(rspec.spec_files)
46
+
47
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
48
+
49
+ # Rails files
50
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
51
+ dsl.watch_spec_files_for(rails.app_files)
52
+ dsl.watch_spec_files_for(rails.views)
53
+
54
+ watch(rails.controllers) do |m|
55
+ [
56
+ rspec.spec.("routing/#{m[1]}_routing"),
57
+ rspec.spec.("controllers/#{m[1]}_controller"),
58
+ rspec.spec.("acceptance/#{m[1]}")
59
+ ]
60
+ end
61
+
62
+ # Rails config changes
63
+ watch(rails.spec_helper) { rspec.spec_dir }
64
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
65
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
66
+
67
+ # Capybara features specs
68
+ watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
69
+
70
+ # Turnip features and steps
71
+ watch(%r{^spec/acceptance/(.+)\.feature$})
72
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
73
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
74
+ end
75
+ end
76
+
77
+ guard :rubocop do
78
+ watch(%r{.+\.rb$})
79
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
80
+ end
81
+
82
+ guard 'yard' do
83
+ watch(%r{app/.+\.rb})
84
+ watch(%r{lib/.+\.rb})
85
+ watch(%r{ext/.+\.c})
5
86
  end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Asana, Inc.
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 CHANGED
@@ -1,16 +1,23 @@
1
1
  # Asana
2
2
 
3
- This gem is a simple Ruby wrapper for the Asana REST API. It uses
4
- [ActiveResource][] to provide a simple, familiar interface for accessing
5
- your Asana account.
3
+ [![Build Status](https://travis-ci.org/Asana/ruby-asana.svg)](https://travis-ci.org/Asana/ruby-asana)
4
+ [![Code Climate](https://codeclimate.com/github/Asana/ruby-asana/badges/gpa.svg)](https://codeclimate.com/github/Asana/ruby-asana)
5
+ [![Dependency Status](https://gemnasium.com/Asana/ruby-asana.svg)](https://gemnasium.com/Asana/ruby-asana)
6
6
 
7
- To learn more, check out the [Asana API Documentation][].
7
+
8
+ A Ruby client for the 1.0 version of the Asana API.
9
+
10
+ Supported rubies:
11
+
12
+ * MRI 2.0.0 up to 2.2.x stable
8
13
 
9
14
  ## Installation
10
15
 
11
16
  Add this line to your application's Gemfile:
12
17
 
13
- gem 'asana'
18
+ ```ruby
19
+ gem 'ruby-asana'
20
+ ```
14
21
 
15
22
  And then execute:
16
23
 
@@ -18,208 +25,330 @@ And then execute:
18
25
 
19
26
  Or install it yourself as:
20
27
 
21
- $ gem install asana
28
+ $ gem install ruby-asana
22
29
 
23
30
  ## Usage
24
31
 
25
- In order to access Asana, you need to provide your [API key][].
32
+ To do anything, you'll need always an instance of `Asana::Client` configured
33
+ with your preferred authentication method (see the Authentication section below
34
+ for more complex scenarios) and other options.
26
35
 
36
+ The most minimal example would be as follows:
27
37
 
28
38
  ```ruby
29
- Asana.configure do |client|
30
- client.api_key = 'your_asana_api_key'
39
+ require 'asana'
40
+
41
+ client = Asana::Client.new do |c|
42
+ c.authentication :api_token, 'my_api_token'
31
43
  end
44
+
45
+ client.workspaces.find_all.first
32
46
  ```
33
47
 
34
- As a sanity check, you can fetch the object representing your user.
48
+ A full-blown customized client using OAuth2 wih a previously obtained refresh
49
+ token, Typhoeus as a Faraday adapter, a custom user agent and custom Faraday
50
+ middleware:
35
51
 
36
52
  ```ruby
37
- Asana::User.me
53
+ require 'asana'
54
+
55
+ client = Asana::Client.new do |c|
56
+ c.authentication :oauth2,
57
+ refresh_token: 'abc',
58
+ client_id: 'bcd',
59
+ client_secret: 'cde',
60
+ redirect_uri: 'http://example.org/auth'
61
+ c.faraday_adapter :typhoeus
62
+ c.configure_faraday { |conn| conn.use SomeFaradayMiddleware }
63
+ end
64
+
65
+ workspace = client.workspaces.find_by_id(12)
66
+ workspace.users
67
+ # => #<Asana::Collection<User> ...>
68
+ client.tags.create_in_workspace(workspace: workspace.id, name: 'foo')
69
+ # => #<Asana::Tag id: ..., name: "foo">
38
70
  ```
39
71
 
40
- ### [Users][]
72
+ All resources are exposed as methods on the `Asana::Client` instance. Check out
73
+ the [documentation for each of them][docs].
74
+
75
+ ### Authentication
41
76
 
42
- > A user object represents an account in Asana that can be given access to
43
- > various workspaces, projects, and tasks.
44
- >
45
- > Like other objects in the system, users are referred to by numerical IDs.
46
- > However, the special string identifier me can be used anywhere a user ID is
47
- > accepted, to refer to the current authenticated user.
77
+ This gem supports authenticating against the Asana API with either an API token or through OAuth2.
48
78
 
49
- **Note:** It is not possible to create, update, or delete a user from
50
- the API.
79
+ #### API Token
51
80
 
52
81
  ```ruby
53
- # Get users from all of your workspaces
54
- users = Asana::User.all
82
+ Asana::Client.new do |c|
83
+ c.authentication :api_token, 'my_api_token'
84
+ end
85
+ ```
55
86
 
56
- # Get a specific user
57
- user = Asana::User.find(:user_id)
87
+ #### OAuth2
58
88
 
59
- # Get the user associated with the API key being used
60
- user = Asana::User.me
89
+ Authenticating through OAuth2 is preferred. There are many ways you can do this.
61
90
 
62
- # Get all users in a given workspace
63
- workspace = Asana::Workspace.find(:workspace_id)
64
- users = workspace.users
65
- ```
91
+ ##### With a plain bearer token (doesn't support auto-refresh)
66
92
 
67
- ### [Workspaces][]
93
+ If you have a plain bearer token obtained somewhere else and you don't mind not
94
+ having your token auto-refresh, you can authenticate with it as follows:
68
95
 
69
- > A workspace is the most basic organizational unit in Asana. All projects
70
- > and tasks have an associated workspace.
96
+ ```ruby
97
+ Asana::Client.new do |c|
98
+ c.authentication :oauth2, bearer_token: 'my_bearer_token'
99
+ end
100
+ ```
71
101
 
72
- **Note:** It is not possible to create or delete a workspace from the API.
102
+ ##### With a refresh token and client credentials
103
+
104
+ If you obtained a refresh token, you can use it together with your client
105
+ credentials to authenticate:
73
106
 
74
107
  ```ruby
75
- # Get all workspaces
76
- workspaces = Asana::Workspace.all
108
+ Asana::Client.new do |c|
109
+ c.authentication :oauth2,
110
+ refresh_token: 'abc',
111
+ client_id: 'bcd',
112
+ client_secret: 'cde',
113
+ redirect_uri: 'http://example.org/auth'
114
+ end
115
+ ```
77
116
 
78
- # Get a specific workspace
79
- workspace = Asana::Workspace.find(:workspace_id)
117
+ ##### With an ::OAuth2::AccessToken object (from `omniauth-asana` for example)
80
118
 
81
- # Get all projects in a given workspace
82
- projects = workspace.projects
119
+ If you use `omniauth-asana` or a browser-based OAuth2 authentication strategy in
120
+ general, possibly because your application is a web application, you can reuse
121
+ those credentials to authenticate with this API client. Here's how to do it from
122
+ the callback method:
83
123
 
84
- # Get all tasks in a given workspace that are assigned to the given user
85
- tasks = workspace.tasks(:user_id)
124
+ ```ruby
125
+ # assuming we're using Sinatra and omniauth-asana
126
+ get '/auth/:name/callback' do
127
+ creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
128
+ strategy = request.env["omniauth.strategy"]
129
+
130
+ # We need to refresh the omniauth OAuth2 token
131
+ access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh!
132
+
133
+ $client = Asana::Client.new do |c|
134
+ c.authentication :oauth2, access_token
135
+ end
136
+
137
+ redirect '/'
138
+ end
139
+ ```
86
140
 
87
- # Get all users with access to a given workspace
88
- users = workspace.users
141
+ See `examples/omniauth_integration.rb` for a working example of this.
89
142
 
90
- # Get all tags in a given workspace
91
- tags = workspace.tags
143
+ ##### Using an OAuth2 offline authentication flow (for CLI applications)
92
144
 
93
- # Create a new task in a given workspace and assign it to the current user
94
- workspace.create_task(:name => 'Get milk from the grocery store')
145
+ If your application can't receive HTTP requests and thus you can't use
146
+ `omniauth-asana`, for example if it's a CLI application, you can authenticate as
147
+ follows:
95
148
 
96
- # Create a new project in a given workspace, current user as a watcher
97
- workspace.create_project(:name => 'Upgrade Asana gem')
149
+ ```ruby
150
+ access_token = Asana::Authentication::OAuth2.offline_flow(client_id: ...,
151
+ client_secret: ...)
152
+ client = Asana::Client.new do |c|
153
+ c.authentication :oauth2, access_token
154
+ end
98
155
 
99
- # Create a new tag in a given workspace
100
- workspace.create_tag(:name => 'Programming')
156
+ client.tasks.find_by_id(12)
101
157
  ```
102
158
 
103
- ### [Projects][]
159
+ This will print an authorization URL on STDOUT, and block until you paste in the
160
+ authorization code, which you can get by visiting that URL and granting the
161
+ necessary permissions.
104
162
 
105
- > A project represents a prioritized list of tasks in Asana. It exists in a
106
- > single workspace and is accessible to a subset of users in that workspace
107
- > depending on its permissions.
163
+ ### Pagination
108
164
 
109
- **Note:** It is not possible to create or delete a project from the API.
165
+ Whenever you ask for a collection of resources, you can provide a number of
166
+ results per page to fetch, between 1 and 100. If you don't provide any, it
167
+ defaults to 20.
110
168
 
111
169
  ```ruby
112
- # Get all projects
113
- projects = Asana::Project.all
170
+ my_tasks = client.tasks.find_by_tag(tag: tag_id, per_page: 5)
171
+ # => #<Asana::Collection<Task> ...>
172
+ ```
173
+
174
+ An `Asana::Collection` is a paginated collection -- it holds the first
175
+ `per_page` results, and a reference to the next page if any.
114
176
 
115
- # Get a specific project
116
- project = Asana::Project.find(:project_id)
177
+ When you iterate an `Asana::Collection`, it'll transparently keep fetching all
178
+ the pages, and caching them along the way:
117
179
 
118
- # Get all projects in a given workspace
119
- workspace = Asana::Workspace.find(:workspace_id)
120
- projects = workspace.projects
180
+ ```ruby
181
+ my_tasks.size # => 23, not 5
182
+ my_tasks.take(14)
183
+ # => [#<Asana::Task ...>, #<Asana::Task ...>, ... until 14]
184
+ ```
121
185
 
122
- # Change the name of a project
123
- project.modify(:name => 'New project name')
186
+ #### Manual pagination
187
+
188
+ If you only want to deal with one page at a time and manually paginate, you can
189
+ get the elements of the current page with `#elements` and ask for the next page
190
+ with `#next_page`, which will return an `Asana::Collection` with the next page
191
+ of elements:
192
+
193
+ ```ruby
194
+ my_tasks.elements # => [#<Asana::Task ...>, #<Asana::Task ...>, ... until 5]
195
+ my_tasks.next_page # => #<Asana::Collection ...>
124
196
  ```
125
197
 
126
- ### [Tasks][]
198
+ #### Lazy pagination
127
199
 
128
- > The task is the basic object around which many operations in Asana are
129
- > centered. In the Asana application, multiple tasks populate the middle
130
- > pane according to some view parameters, and the set of selected tasks
131
- > determine the more detailed information presented in the details pane.
200
+ Because an `Asana::Collection` represents the entire collection, it is often
201
+ handy to just take what you need from it, rather than let it fetch all its
202
+ contents from the network. You can accomplish this by turning it into a lazy
203
+ collection with `#lazy`:
132
204
 
133
205
  ```ruby
134
- # Get all tasks in a given project
135
- project = Asana::Project.find(:project_id)
136
- tasks = project.tasks
206
+ # let my_tasks be an Asana::Collection of 10 pages of 100 elements each
207
+ my_tasks.lazy.drop(120).take(15).to_a
208
+ # Fetches only 2 pages, enough to get elements 120 to 135
209
+ # => [#<Asana::Task ...>, #<Asana::Task ...>, ...]
210
+ ```
137
211
 
138
- # Get all tasks in a given workspace
139
- workspace = Asana::Workspace.find(:workspace_id)
140
- tasks = workspace.tasks
212
+ ### Error handling
141
213
 
142
- # Get all tasks with a given tag
143
- tag = Asana::Tag.find(:tag_id)
144
- tasks = tag.tasks
214
+ In any request against the Asana API, there a number of errors that could
215
+ arise. Those are well documented in the [Asana API Documentation][apidocs], and
216
+ are represented as exceptions under the namespace `Asana::Errors`.
145
217
 
146
- # Get all stories for a given task
147
- task = tasks.first
148
- stories = task.stories
218
+ All errors are subclasses of `Asana::Errors::APIError`, so make sure to rescue
219
+ instances of this class if you want to handle them yourself.
149
220
 
150
- # Create a new task in a given workspace and assign it to the current user
151
- workspace.create_task(:name => 'Get milk from the grocery store')
221
+ ### I/O options
152
222
 
153
- # Create a new story for the given task
154
- task.create_story(story_settings)
223
+ All requests (except `DELETE`) accept extra I/O options
224
+ [as documented in the API docs][io]. Just pass an extra `options` hash to any
225
+ request:
155
226
 
156
- # Add a project to the task (tasks can be in multiple projects)
157
- task.add_project(project.id)
227
+ ```ruby
228
+ client.tasks.find_by_id(12, options: { expand: ['workspace'] })
158
229
  ```
159
230
 
160
- ### [Tags][]
231
+ ### Attachment uploading
161
232
 
162
- > A tag is a label that can be attached to any task in Asana. It exists in
163
- > a single workspace or organization.
164
- >
165
- > Tags have some metadata associated with them, but it is possible that we will
166
- > simplify them in the future so it is not encouraged to rely too heavily on
167
- > it. Unlike projects, tags do not provide any ordering on the tasks they are
168
- > associated with.
233
+ To attach a file to a task or a project, you just need its absolute path on your
234
+ filesystem and its MIME type, and the file will be uploaded for you:
169
235
 
170
236
  ```ruby
171
- # Get all tags in a given workspace
172
- workspace = Asana::Workspace.find(:workspace_id)
173
- tags = workspace.tags
237
+ task = client.tasks.find_by_id(12)
238
+ attachment = task.attach(filename: '/absolute/path/to/my/file.png',
239
+ mime: 'image/png')
240
+ attachment.name # => 'file.png'
241
+ ```
242
+
243
+ ### Event streams
174
244
 
175
- # Get all tasks with a given tag
176
- tag = Asana::Tag.find(:tag_id)
177
- tasks = tag.tasks
245
+ To subscribe to an event stream of a task or a project, just call `#events` on
246
+ it:
178
247
 
179
- # Create a new tag in a given workspace
180
- workspace.create_tag(:name => 'Programming')
248
+ ```ruby
249
+ task = client.tasks.find_by_id(12)
250
+ task.events # => #<Asana::Events ...>
181
251
 
182
- # Update a tag
183
- tag = Asana::Tag.find(:tag_id)
184
- tag.modify(:name => 'Development')
252
+ # You can do the same with only the task id:
253
+ events = client.events.for(task.id)
185
254
  ```
186
255
 
187
- ### [Stories][]
256
+ An `Asana::Events` object is an infinite collection of `Asana::Event`
257
+ instances. Be warned that if you call `#each` on it, it will block forever!
258
+
259
+ Note that, by default, an event stream will wait at least 1 second between
260
+ polls, but that's configurable with the `wait` parameter:
261
+
262
+ ```ruby
263
+ # wait at least 3 and a half seconds between each poll to the API
264
+ task.events(wait: 3.5) # => #<Asana::Events ...>
265
+ ```
188
266
 
189
- > A story represents an activity associated with an object in the Asana
190
- > system. Stories are generated by the system whenever users take actions
191
- > such as creating or assigning tasks, or moving tasks between projects.
192
- > Comments are also a form of user-generated story.
267
+ There are some interesting things you can do with an event stream, as it is a
268
+ normal Ruby Enumerable. Read below to get some ideas.
193
269
 
194
- **Note:** It is not possible to update or delete a story from the API.
270
+ #### Subscribe to the event stream with a callback, polling every 2 seconds
195
271
 
196
272
  ```ruby
197
- # Get all stories for a given task
198
- project = Asana::Project.find(:project_id)
199
- task = project.tasks.first
200
- stories = task.stories
273
+ # Run this in another thread so that we don't block forever
274
+ events = client.tasks.find_by_id(12).events(wait: 2)
275
+ Thread.new do
276
+ events.each do |event|
277
+ notify_someone "New event arrived! #{event}"
278
+ end
279
+ end
280
+ ```
281
+
282
+ #### Make the stream lazy and filter it by a specific pattern
201
283
 
202
- # Get a specific story
203
- story = Story.find(:story_id)
284
+ To do that we need to call `#lazy` on the `Events` instance, just like with any
285
+ other `Enumerable`.
204
286
 
205
- # Create a new story for the given task/project
206
- task.create_story(story_settings)
287
+ ```ruby
288
+ events = client.tasks.find_by_id(12).events
289
+ only_change_events = events.lazy.select { |event| event.action == 'changed' }
290
+ Thread.new do
291
+ only_change_events.each do |event|
292
+ notify_someone "New change event arrived! #{event}"
293
+ end
294
+ end
207
295
  ```
208
296
 
297
+ ## Development
298
+
299
+ You'll need Ruby 2.1+ and Node v0.10.26+ / NPM 1.4.3+ installed.
300
+
301
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
302
+ `bin/console` for an interactive prompt that will allow you to experiment.
303
+
304
+ Run the build with `rake`. This is equivalent to:
305
+
306
+ $ rake spec && rake rubocop && rake yard
307
+
308
+ To install this gem onto your local machine, run `bundle exec rake install`.
309
+
310
+ ## Releasing a new version
311
+
312
+ To release a new version, run either of these commands:
313
+
314
+ rake bump:patch
315
+ rake bump:minor
316
+ rake bump:major
317
+
318
+ This will: update `lib/asana/version.rb`, commit and tag the commit. Then you
319
+ just need to `push --tags` to let Travis build and release the new version to
320
+ Rubygems:
321
+
322
+ git push --tags
323
+
324
+ ### Code generation
325
+
326
+ The specific Asana resource classes (`Tag`, `Workspace`, `Task`, etc) are
327
+ generated code, hence they shouldn't be modified by hand. The code that
328
+ generates it lives in `lib/templates/resource.ejs`, and is tested by generating
329
+ `spec/templates/unicorn.rb` and running `spec/templates/unicorn_spec.rb` as part
330
+ of the build.
331
+
332
+ If you wish to make changes on the code generation script:
333
+
334
+ 1. Add/modify a spec on `spec/templates/unicorn_spec.rb`
335
+ 2. Add your new feature or change to `lib/templates/resource.ejs`
336
+ 3. Run `rake` or, more granularly, `rake codegen && rspec
337
+ spec/templates/unicorn_spec.rb`
338
+
339
+ Once you're sure your code works, submit a pull request and ask the maintainer
340
+ to make a release, as they'll need to run a release script from the
341
+ [asana-api-meta][meta] repository.
342
+
209
343
  ## Contributing
210
344
 
211
- 1. Fork it
345
+ 1. Fork it ( https://github.com/[my-github-username]/asana/fork )
212
346
  2. Create your feature branch (`git checkout -b my-new-feature`)
213
- 3. Commit your changes (`git commit -am 'Added some feature'`)
347
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
214
348
  4. Push to the branch (`git push origin my-new-feature`)
215
- 5. Create new Pull Request
216
-
217
- [API key]: http://app.asana.com/-/account_api
218
- [ActiveResource]: http://api.rubyonrails.org/classes/ActiveResource/Base.html
219
- [Asana API Documentation]: http://developer.asana.com/documentation/
220
- [Users]: http://developer.asana.com/documentation/#users
221
- [Workspaces]: http://developer.asana.com/documentation/#workspaces
222
- [Projects]: http://developer.asana.com/documentation/#projects
223
- [Tasks]: http://developer.asana.com/documentation/#tasks
224
- [Stories]: http://developer.asana.com/documentation/#stories
225
- [Tags]: http://developer.asana.com/documentation/#tags
349
+ 5. Create a new Pull Request
350
+
351
+ [apidocs]: https://asana.com/developers
352
+ [io]: https://asana.com/developers/documentation/getting-started/input-output-options
353
+ [docs]: https://asana.github.com/ruby-asana
354
+ [meta]: https://github.com/asana/asana-api-meta