ketchup 1.0.0

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.
@@ -0,0 +1,92 @@
1
+ # A collection of meeting for the current profile, which allows for the creation
2
+ # of new meetings. It's really just a slightly special subclass of Array.
3
+ #
4
+ class Ketchup::MeetingArray < Array
5
+ # Create a new array from a given api connection. This will make the request
6
+ # to the API to retrieve all meetings for the profile.
7
+ #
8
+ # This isn't something you need to call yourself - just call the meetings
9
+ # method on your profile object instead.
10
+ #
11
+ # @example
12
+ # meetings = Ketchup::MeetingArray.new api
13
+ #
14
+ # @param [Ketchup::API] api an API instance
15
+ # @see Ketchup::Profile#meetings
16
+ #
17
+ def initialize(api)
18
+ @api = api
19
+
20
+ replace @api.get('/meetings.json').collect { |hash|
21
+ Ketchup::Meeting.new(@api, hash)
22
+ }
23
+ end
24
+
25
+ # Create a new unsaved meeting object. The only parameters you really need to
26
+ # worry about are the title and date - the rest are optional, but mentioned in
27
+ # the notes for the Meeting class.
28
+ #
29
+ # Don't forget: the parameter keys need to be strings, not symbols.
30
+ #
31
+ # @example
32
+ # profile.meetings.build 'title' => 'Job Interview', 'date' => 'Tomorrow'
33
+ #
34
+ # @param [Hash] params The meeting's details
35
+ # @return [Ketchup::Meeting] An unsaved meeting
36
+ # @see Ketchup::Meeting
37
+ #
38
+ def build(params = {})
39
+ meeting = Ketchup::Meeting.new @api, params
40
+ push meeting
41
+ meeting
42
+ end
43
+
44
+ # Create a new (saved) meeting object. The only parameters you really need to
45
+ # worry about are the title and date - the rest are optional, but mentioned in
46
+ # the notes for the Meeting class.
47
+ #
48
+ # Don't forget: the parameter keys need to be strings, not symbols.
49
+ #
50
+ # @example
51
+ # profile.meetings.create 'title' => 'Job Interview', 'date' => 'Tomorrow'
52
+ #
53
+ # @param [Hash] params The meeting's details
54
+ # @return [Ketchup::Meeting] A saved meeting
55
+ # @see Ketchup::Meeting
56
+ #
57
+ def create(params = {})
58
+ meeting = build(params)
59
+ meeting.save
60
+ meeting
61
+ end
62
+
63
+ # Requests a set of meetings that happen on days after today from the API.
64
+ #
65
+ # @return [Array] An array of upcoming meetings
66
+ #
67
+ def upcoming
68
+ @upcoming ||= @api.get('/meetings/upcoming.json').collect { |hash|
69
+ Ketchup::Meeting.new(@api, hash)
70
+ }
71
+ end
72
+
73
+ # Requests a set of meetings that have already happened from the API.
74
+ #
75
+ # @return [Array] An array of previous meetings
76
+ #
77
+ def previous
78
+ @previous ||= @api.get('/meetings/previous.json').collect { |hash|
79
+ Ketchup::Meeting.new(@api, hash)
80
+ }
81
+ end
82
+
83
+ # Requests a set of meetings that will happen today from the API.
84
+ #
85
+ # @return [Array] An array of today's meetings
86
+ #
87
+ def today
88
+ @today ||= @api.get('/meetings/todays.json').collect { |hash|
89
+ Ketchup::Meeting.new(@api, hash)
90
+ }
91
+ end
92
+ end
@@ -0,0 +1,73 @@
1
+ # Represents a note for list/agenda item in a meeting. Only the content is
2
+ # editable.
3
+ #
4
+ # If you want to change the order of notes, you can do that using
5
+ # Ketchup::NoteArray (the item's note array).
6
+ #
7
+ class Ketchup::Note
8
+ attr_reader :shortcode_url, :updated_at, :created_at, :item_id, :position,
9
+ :item, :api
10
+ attr_accessor :content
11
+
12
+ # Create a new note for a given item, using an existing api connection. You
13
+ # generally won't want to call this yourself - the better interface to create
14
+ # new notes is via a item's note array.
15
+ #
16
+ # However, if you are doing some internal hacking, it's worth noting that all
17
+ # keys for parameters in the hash should be strings, not symbols.
18
+ #
19
+ # @example
20
+ # Ketchup::Note.new api, item, 'content' => "Motion passed."
21
+ #
22
+ # @param [Ketchup::API] api The API connection
23
+ # @param [Ketchup::Item] item The item the note will belong to
24
+ # @param [Hash] params Any other details for the note.
25
+ # @see Ketchup::NoteArray#build
26
+ # @see Ketchup::NoteArray#create
27
+ #
28
+ def initialize(api, item, params = {})
29
+ @api = api
30
+ @item = item
31
+
32
+ overwrite params
33
+ end
34
+
35
+ # Indicates whether the note exists on the server yet.
36
+ #
37
+ # @return [Boolean] True if the note does not exist on the server.
38
+ #
39
+ def new_record?
40
+ shortcode_url.nil?
41
+ end
42
+
43
+ # Saves the note to the server, whether it's a new note or an existing one.
44
+ #
45
+ def save
46
+ if new_record?
47
+ overwrite @api.post("/items/#{item.shortcode_url}/notes.json",
48
+ 'content' => content)
49
+ else
50
+ overwrite @api.put("/notes/#{shortcode_url}.json", 'content' => content)
51
+ end
52
+ end
53
+
54
+ # Deletes the note from the server. If the note has never been saved, this
55
+ # method will do nothing at all.
56
+ #
57
+ def destroy
58
+ return if new_record?
59
+
60
+ @api.delete "/notes/#{shortcode_url}.json"
61
+ end
62
+
63
+ private
64
+
65
+ def overwrite(attributes = {})
66
+ @shortcode_url = attributes['shortcode_url']
67
+ @created_at = attributes['created_at']
68
+ @updated_at = attributes['updated_at']
69
+ @item_id = attributes['item_id']
70
+ @position = attributes['position']
71
+ @content = attributes['content']
72
+ end
73
+ end
@@ -0,0 +1,85 @@
1
+ # A collection of notes for a specific item, and allows for the creation of
2
+ # new notes, and re-ordering of the full set of notes. It's really just a
3
+ # slightly special subclass of Array.
4
+ #
5
+ class Ketchup::NoteArray < Array
6
+ # Create a new NoteArray for a given api, item, and set of notes. The set
7
+ # of notes should be hashes taken from the API server, not actual note
8
+ # objects.
9
+ #
10
+ # Like many of the methods in this library, you really don't need to call this
11
+ # one yourself. An item object will create its own NoteArray without any
12
+ # help.
13
+ #
14
+ # @example
15
+ # Ketchup::NoteArray.new api, item, [{'content' => 'foo', #... }]
16
+ #
17
+ # @param [Ketchup::API] api A connected API instance
18
+ # @param [Ketchup::Item] item The item all notes are attached to.
19
+ # @param [Array] items An array of hashes that map to attributes of notes.
20
+ #
21
+ def initialize(api, item, notes = [])
22
+ @api = api
23
+ @item = item
24
+
25
+ replace notes.collect { |hash|
26
+ Ketchup::Note.new(@api, @item, hash)
27
+ }
28
+ end
29
+
30
+ # Create a new unsaved note object. The only parameter you really need to
31
+ # worry about is the content - the rest is all internal values, managed by the
32
+ # API server.
33
+ #
34
+ # @example
35
+ # item.notes.build 'content' => 'Targets are being met.'
36
+ #
37
+ # @param [Hash] params The note's details
38
+ # @return [Ketchup::Note] An unsaved note
39
+ #
40
+ def build(params = {})
41
+ note = Ketchup::Note.new @api, @item, params
42
+ push note
43
+ note
44
+ end
45
+
46
+ # Create a new (saved) note object. The only parameter you really need to
47
+ # worry about is the content - the rest is all internal values, managed by the
48
+ # API server.
49
+ #
50
+ # @example
51
+ # item.notes.create 'content' => 'Targets are being met.'
52
+ #
53
+ # @param [Hash] params The note's details
54
+ # @return [Ketchup::Note] A saved note
55
+ #
56
+ def create(params = {})
57
+ note = build(params)
58
+ note.save
59
+ note
60
+ end
61
+
62
+ # Re-order the notes in this array, by passing them through in the order you
63
+ # would prefer. This makes the change directly to the API server, as well as
64
+ # within this array.
65
+ #
66
+ # Make sure you're passing in all the notes that are in this array, and no
67
+ # others. This can't be used as a shortcut to add new notes.
68
+ #
69
+ # @example
70
+ # item.notes.reorder second_note, fourth_note, first_note, third_note
71
+ #
72
+ # @param [Ketchup::Note] notes The notes of the array, in a new order.
73
+ # @raise ArgumentError if you don't provide the exact same set of notes.
74
+ #
75
+ def reorder(*notes)
76
+ if notes.collect(&:shortcode_url).sort != collect(&:shortcode_url).sort
77
+ raise ArgumentError, 'cannot sort a different set of notes'
78
+ end
79
+
80
+ @api.put "/items/#{@item.shortcode_url}/sort_notes.json", {
81
+ 'notes' => notes.collect(&:shortcode_url)
82
+ }
83
+ replace notes
84
+ end
85
+ end
@@ -0,0 +1,125 @@
1
+ # A user profile, which provides an interface to the user's meetings and
2
+ # projects. You can either create it yourself, using an email and password, or
3
+ # through the Ketchup module's authenticate method - they're exactly the same.
4
+ #
5
+ # This is the central object for all of your operations through the Ketchup API.
6
+ # You don't need to create or manually request any other objects, but instead,
7
+ # access them all through the collections on this class.
8
+ #
9
+ # @example
10
+ # profile = Ketchup::Profile.load 'foo@bar.com', 'secret'
11
+ # profile.meetings.each do |meeting|
12
+ # puts meeting.title
13
+ # end
14
+ #
15
+ class Ketchup::Profile
16
+ attr_reader :api, :single_access_token
17
+ attr_accessor :name, :timezone, :email
18
+
19
+ # Set up a connection to an existing profile.
20
+ #
21
+ # @example
22
+ # profile = Ketchup::Profile.load 'foo@bar.com', 'secret'
23
+ #
24
+ # @param [String] email The user's email address
25
+ # @param [String] password The user's passwords
26
+ # @return [Ketchup::Profile] A new profile instance
27
+ # @raise [Ketchup::AccessDenied] when the email or password is incorrect
28
+ #
29
+ def self.load(email, password)
30
+ Ketchup::Profile.new Ketchup::API.new(email, password)
31
+ end
32
+
33
+ # Create a completely new user account. You (currently) don't need to have
34
+ # any existing authentication to the server to call this method - just specify
35
+ # the new user's email address and password.
36
+ #
37
+ # Other extra parameters are the name and timezone of the user account, but
38
+ # these are optional.
39
+ #
40
+ # Please note: This method doesn't return a new profile, just sets the account
41
+ # up.
42
+ #
43
+ # @example
44
+ # Ketchup::Profile.create 'bar@foo.com', 'swordfish', 'name' => 'Bruce'
45
+ #
46
+ # @param [String] email The email address of the new user.
47
+ # @param [String] password The chosen password of the new user.
48
+ # @param [Hash] options Other optional user details, such as the name and
49
+ # timezone.
50
+ #
51
+ def self.create(email, password, options = {})
52
+ Ketchup::API.post '/users.json', :body => options.merge(
53
+ 'email' => email,
54
+ 'password' => password
55
+ )
56
+ end
57
+
58
+ # Set up a new profile object with an active API connection.
59
+ #
60
+ # @param [Ketchup::API] api An authenticated API object
61
+ #
62
+ def initialize(api)
63
+ @api = api
64
+
65
+ overwrite @api.get('/profile.json')
66
+ end
67
+
68
+ # Reset the meeting and project objects of this profile, allowing them to
69
+ # be reloaded from the server when next requested.
70
+ #
71
+ def reload!
72
+ @meetings = nil
73
+ @projects = nil
74
+ end
75
+
76
+ # Get an array of meetings attached to this profile. Note that the returned
77
+ # object is actually an instance of Ketchup::MeetingArray, which has helper
78
+ # methods for creating and re-ordering meetings.
79
+ #
80
+ # @return [Ketchup::MeetingArray] The collection of meetings.
81
+ #
82
+ def meetings
83
+ @meetings ||= Ketchup::MeetingArray.new api
84
+ end
85
+
86
+ # Get an array of projects attached to this profile. This is not a special
87
+ # project array, as you can only edit projects, not create them. They are
88
+ # created explicitly from meetings.
89
+ #
90
+ # @return [Array] The collection of projects.
91
+ #
92
+ def projects
93
+ @projects ||= api.get('/projects.json').collect { |hash|
94
+ Ketchup::Project.new(api, hash)
95
+ }
96
+ end
97
+
98
+ # Saves any profile changes to the name, timezone and/or email. This does not
99
+ # save any changes made to underlying meetings and projects.
100
+ #
101
+ def save
102
+ overwrite @api.put('/profile.json',
103
+ 'name' => name,
104
+ 'timezone' => timezone,
105
+ 'email' => email
106
+ )
107
+ end
108
+
109
+ # Change the password for this profile account.
110
+ #
111
+ # @param [String] password The new password for the account.
112
+ #
113
+ def change_password(password)
114
+ @api.put '/profile.json', 'password' => password
115
+ end
116
+
117
+ private
118
+
119
+ def overwrite(attributes = {})
120
+ @single_access_token = attributes['single_access_token']
121
+ @name = attributes['name']
122
+ @timezone = attributes['timezone']
123
+ @email = attributes['email']
124
+ end
125
+ end
@@ -0,0 +1,51 @@
1
+ # Represents a project, which meetings can be grouped under.
2
+ #
3
+ # Projects cannot be created manually - just create meetings with project names,
4
+ # and the corresponding projects will be created if they don't already exist.
5
+ #
6
+ # You can, however, change the name of a project.
7
+ #
8
+ class Ketchup::Project
9
+ attr_accessor :name
10
+ attr_reader :shortcode_url, :created_at, :updated_at
11
+
12
+ # Set up a new Project object - although there's not much point calling this
13
+ # yourself, as it's only useful when populating with data from the server.
14
+ #
15
+ # @param [Ketchup::API] api An established API connection.
16
+ # @param [Hash] params The project details
17
+ #
18
+ def initialize(api, params = {})
19
+ @api = api
20
+
21
+ overwrite params
22
+ end
23
+
24
+ # Saves any changes to the project's name.
25
+ #
26
+ def save
27
+ overwrite @api.put("/projects/#{shortcode_url}.json", 'name' => name)
28
+ end
29
+
30
+ # Gets all the meetings tied to this particular project. This is not a special
31
+ # array, so it's best to create new meetings for a given project via the
32
+ # profile's meetings collection object instead.
33
+ #
34
+ # @return [Array] Meetings associated with this project.
35
+ #
36
+ def meetings
37
+ @meetings ||= @api.get("/projects/#{shortcode_url}/meetings.json").
38
+ collect { |hash|
39
+ Ketchup::Meeting.new @api, hash
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def overwrite(attributes = {})
46
+ @shortcode_url = attributes['shortcode_url']
47
+ @created_at = attributes['created_at']
48
+ @updated_at = attributes['updated_at']
49
+ @name = attributes['name']
50
+ end
51
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe Ketchup::API do
4
+ describe '#initialize' do
5
+ it "should return a new API object on success" do
6
+ FakeWeb.register_uri :get, /#{KetchupAPI}\/profile.json$/,
7
+ :body => '{"single_access_token":"foo"}'
8
+
9
+ Ketchup::API.new('user@domain.com', 'secret').should be_a(Ketchup::API)
10
+ end
11
+
12
+ it "should set the access token on success" do
13
+ stub_initial_api_request
14
+
15
+ Ketchup::API.new('user@domain.com', 'secret').
16
+ access_token.should == 'y3chHxh9Qk1Drkg1j0Bw'
17
+ end
18
+
19
+ it "should raise an exception on failure" do
20
+ FakeWeb.register_uri :get, /#{KetchupAPI}\/profile.json$/,
21
+ :body => 'Access Denied'
22
+
23
+ lambda {
24
+ Ketchup::API.new('user@domain.com', 'secret')
25
+ }.should raise_error(Ketchup::AccessDenied)
26
+ end
27
+ end
28
+
29
+ describe '#get' do
30
+ before :each do
31
+ stub_initial_api_request
32
+ @api = Ketchup::API.new('user@domain.com', 'secret')
33
+ end
34
+
35
+ it "should include the access token" do
36
+ Ketchup::API.should_receive(:get) do |path, options|
37
+ options[:query][:u].should == 'y3chHxh9Qk1Drkg1j0Bw'
38
+ end
39
+
40
+ @api.get '/'
41
+ end
42
+
43
+ it "should send through any provided options" do
44
+ Ketchup::API.should_receive(:get) do |path, options|
45
+ options[:query][:page].should == 4
46
+ end
47
+
48
+ @api.get '/', :page => 4
49
+ end
50
+ end
51
+
52
+ describe '#post' do
53
+ before :each do
54
+ stub_initial_api_request
55
+ @api = Ketchup::API.new('user@domain.com', 'secret')
56
+ end
57
+
58
+ it "should include the access token" do
59
+ Ketchup::API.should_receive(:post) do |path, options|
60
+ options[:body][:u].should == 'y3chHxh9Qk1Drkg1j0Bw'
61
+ end
62
+
63
+ @api.post '/'
64
+ end
65
+
66
+ it "should send through any provided options" do
67
+ Ketchup::API.should_receive(:post) do |path, options|
68
+ options[:body][:page].should == 4
69
+ end
70
+
71
+ @api.post '/', :page => 4
72
+ end
73
+ end
74
+
75
+ describe '#put' do
76
+ before :each do
77
+ stub_initial_api_request
78
+ @api = Ketchup::API.new('user@domain.com', 'secret')
79
+ end
80
+
81
+ it "should include the access token" do
82
+ Ketchup::API.should_receive(:put) do |path, options|
83
+ options[:body][:u].should == 'y3chHxh9Qk1Drkg1j0Bw'
84
+ end
85
+
86
+ @api.put '/'
87
+ end
88
+
89
+ it "should send through any provided options" do
90
+ Ketchup::API.should_receive(:put) do |path, options|
91
+ options[:body][:page].should == 4
92
+ end
93
+
94
+ @api.put '/', :page => 4
95
+ end
96
+ end
97
+
98
+ describe '#delete' do
99
+ before :each do
100
+ stub_initial_api_request
101
+ @api = Ketchup::API.new('user@domain.com', 'secret')
102
+ end
103
+
104
+ it "should include the access token" do
105
+ Ketchup::API.should_receive(:delete) do |path, options|
106
+ options[:body][:u].should == 'y3chHxh9Qk1Drkg1j0Bw'
107
+ end
108
+
109
+ @api.delete '/'
110
+ end
111
+
112
+ it "should send through any provided options" do
113
+ Ketchup::API.should_receive(:delete) do |path, options|
114
+ options[:body][:page].should == 4
115
+ end
116
+
117
+ @api.delete '/', :page => 4
118
+ end
119
+ end
120
+ end