neoneo 0.1.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.
- data/History.txt +6 -0
- data/Manifest.txt +7 -0
- data/README.txt +88 -0
- data/Rakefile +19 -0
- data/lib/neoneo.rb +472 -0
- data/spec/neoneo_spec.rb +4 -0
- data/spec/spec_helper.rb +9 -0
- data.tar.gz.sig +0 -0
- metadata +102 -0
- metadata.gz.sig +0 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
= NeoNeo
|
2
|
+
|
3
|
+
== DESCRIPTION:
|
4
|
+
|
5
|
+
NeoNeo is a Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects.
|
6
|
+
|
7
|
+
It uses mechanize and tends to be a bit slow but hey, it's the only ready to use
|
8
|
+
library today so enjoy it, improve it or go away ;)
|
9
|
+
|
10
|
+
== SYNOPSIS:
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require 'neoneo'
|
14
|
+
|
15
|
+
project = Neoneo::User.new('user name', 'password').projects.find('My Project')
|
16
|
+
project.name = "Cool project"
|
17
|
+
project.save
|
18
|
+
|
19
|
+
p project.tasks.map {|t| t.name}
|
20
|
+
|
21
|
+
project.categories.find('An existing Category').add_task("Cool new Task",
|
22
|
+
:assign_to => 'Some Username')
|
23
|
+
|
24
|
+
project.add_task("Another way to add a task and notify ANYONE ;)",
|
25
|
+
:notify => project.members, :category => "Some Category")
|
26
|
+
|
27
|
+
== REQUIREMENTS:
|
28
|
+
|
29
|
+
* Mechanize (http://mechanize.rubyforge.org/mechanize/)
|
30
|
+
|
31
|
+
== INSTALL:
|
32
|
+
|
33
|
+
* sudo gem install neoneo
|
34
|
+
|
35
|
+
== PERFORMANCE:
|
36
|
+
|
37
|
+
As No Kahuna does not provide a real API to their services Neoneo wraps the
|
38
|
+
normal HTML pages as you can see them in your browser. This means not thaaaat
|
39
|
+
speedy performance. Especially because the No Kahuna guys using Rails cool
|
40
|
+
CSRF avoiding technology and deliver any form with a token which you have to
|
41
|
+
sent back to the server to confirm that you're not working on a stolen session.
|
42
|
+
Due to this e.g. the login procedure consists of THREE HTTP request :/
|
43
|
+
1. Set the language of the interface to english to allow Neoneo to parse any
|
44
|
+
messages correctly
|
45
|
+
2. Get the login form (with that token)
|
46
|
+
3. Post that login form
|
47
|
+
|
48
|
+
But I've tried hard to suck as much information as possible out of any HTML
|
49
|
+
page Neoneo is receiving and to lazy-load most of the details.
|
50
|
+
E.g. when you log in Neoneo can scan all your projects etc from the initial page
|
51
|
+
you get after a login. So no second (or forth to be correct ;) ) request is
|
52
|
+
needed to get a project list. And if you would like to get all the members of
|
53
|
+
a specific project Neoneo checks if they are already present, if not the
|
54
|
+
projects detail page is loaded and all the members are gatherd (along with any
|
55
|
+
other useful information from that page). If you call the members method again,
|
56
|
+
no new request is needed.
|
57
|
+
|
58
|
+
Just want to let you know all this. Neoneo is usable but it's more like a
|
59
|
+
No Kahuna Information Delivery Bus than a No Kahuna Dragster!
|
60
|
+
|
61
|
+
== Etymology
|
62
|
+
Neoneo as the hawaiian word for chaos is just what is logical caused by
|
63
|
+
No Kahuna ;)
|
64
|
+
|
65
|
+
== LICENSE:
|
66
|
+
|
67
|
+
(The MIT License)
|
68
|
+
|
69
|
+
Copyright (c) 2008 FIX
|
70
|
+
|
71
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
72
|
+
a copy of this software and associated documentation files (the
|
73
|
+
'Software'), to deal in the Software without restriction, including
|
74
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
75
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
76
|
+
permit persons to whom the Software is furnished to do so, subject to
|
77
|
+
the following conditions:
|
78
|
+
|
79
|
+
The above copyright notice and this permission notice shall be
|
80
|
+
included in all copies or substantial portions of the Software.
|
81
|
+
|
82
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
83
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
84
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
85
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
86
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
87
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
88
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require 'mechanize'
|
6
|
+
require './lib/neoneo.rb'
|
7
|
+
|
8
|
+
Hoe.new('neoneo', Neoneo::VERSION) do |p|
|
9
|
+
p.rubyforge_name = "kickassrb"
|
10
|
+
p.name = "neoneo"
|
11
|
+
p.author = "Thorben Schröder"
|
12
|
+
p.description = "Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects."
|
13
|
+
p.email = 'thorben@fetmab.net'
|
14
|
+
p.summary = "Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects."
|
15
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
16
|
+
p.extra_deps << ['mechanize'," >=0.8.4"]
|
17
|
+
end
|
18
|
+
|
19
|
+
# vim: syntax=Ruby
|
data/lib/neoneo.rb
ADDED
@@ -0,0 +1,472 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module Neoneo
|
4
|
+
require 'rubygems'
|
5
|
+
require 'mechanize'
|
6
|
+
|
7
|
+
require 'hpricot_extensions'
|
8
|
+
require 'utils'
|
9
|
+
|
10
|
+
|
11
|
+
BASE_URL = 'http://nokahuna.com/'
|
12
|
+
PROJECT_URL = "#{BASE_URL}projects/"
|
13
|
+
VERSION = '0.1.0'
|
14
|
+
|
15
|
+
# A Neoneo::AuthenticationError is thrown whenever No Kahuna reports, that
|
16
|
+
# you're not logged in properly.
|
17
|
+
class AuthenticationError < StandardError; end
|
18
|
+
|
19
|
+
# The default/fallback Error in NeoNeo
|
20
|
+
#
|
21
|
+
# Neoneo::Error is the default error that is thown any time No Kahuna reports
|
22
|
+
# and error which Neoneo is not able to handle properly. Maybe because there
|
23
|
+
# is no error handler for that kind of error implemented or because No Kahuna
|
24
|
+
# changed it's interface and Neoneo has not yet been updated to reflect those
|
25
|
+
# changes
|
26
|
+
class Error < ArgumentError; end
|
27
|
+
|
28
|
+
# Normal array with a few select extensions
|
29
|
+
class SingleSelectArray < Array
|
30
|
+
|
31
|
+
# Find an item in the array by it's name
|
32
|
+
def find(name)
|
33
|
+
self.select {|item| item.name == name}.first
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find an item in the array by it's name when value is a string.
|
37
|
+
# If the passed value is a Member or Category object just return that and
|
38
|
+
# if none of those rules apply return nil.
|
39
|
+
#
|
40
|
+
# This method allows the user e.g. to assign new task by just using the
|
41
|
+
# name of the project member and not it's corresponding Member object.
|
42
|
+
def find_or_use(value)
|
43
|
+
case value
|
44
|
+
when String
|
45
|
+
result = find(value)
|
46
|
+
when Member, Category, Project
|
47
|
+
result = value
|
48
|
+
else
|
49
|
+
result = nil
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Wrapper around the WWW::Mechanize object to allow an easy and DRY error
|
56
|
+
# handling.
|
57
|
+
#
|
58
|
+
# Ath the moment only the get, post and submit methods are subject of this
|
59
|
+
# error handling.
|
60
|
+
class Agent < WWW::Mechanize
|
61
|
+
def get(url)
|
62
|
+
page = super(url)
|
63
|
+
handle_errors(page)
|
64
|
+
end
|
65
|
+
|
66
|
+
def post(url, options = {})
|
67
|
+
page = super(url, options)
|
68
|
+
handle_errors(page)
|
69
|
+
end
|
70
|
+
|
71
|
+
def submit(form)
|
72
|
+
page = super(form)
|
73
|
+
handle_errors(page)
|
74
|
+
end
|
75
|
+
|
76
|
+
# This methos actually does the error handling.
|
77
|
+
#
|
78
|
+
# When an error message is found in the response from No Kahuna it's
|
79
|
+
# text determines which error is thrown.
|
80
|
+
# If the error message does not match any of the specific messages Neoneo
|
81
|
+
# tries to catch, a default Neoneo::Error is thrown.
|
82
|
+
def handle_errors(page)
|
83
|
+
return page if page.instance_of? WWW::Mechanize::File
|
84
|
+
|
85
|
+
errors = page.search('div#flash.error p').map {|e| e.innerText}
|
86
|
+
errors.each do |error|
|
87
|
+
case error
|
88
|
+
when 'Invalid login or password.'
|
89
|
+
raise AuthenticationError
|
90
|
+
else
|
91
|
+
raise Error.new(e)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
page
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# The starting point for any use of the NeoNeo library.
|
100
|
+
#
|
101
|
+
# Other than a Neoneo::Member iy represents a user of No Kahuna of wich you
|
102
|
+
# have the full login credentials.
|
103
|
+
#
|
104
|
+
# To initialize a connection to No Kahuna start with:
|
105
|
+
#
|
106
|
+
# Neoneo::User.new('User Name', 'Password')
|
107
|
+
#
|
108
|
+
# Neoneo then loggs you in to No Kahuna and gathers some first informations
|
109
|
+
# about your projects, task counts and so on.
|
110
|
+
#
|
111
|
+
# Unfortunately the initialization process needs to do actually three HTTP
|
112
|
+
# requests at the moment. First it sets your language to English, than it
|
113
|
+
# has to get the login form to be aware of the CSRF id to actually log you
|
114
|
+
# in in a third request, the submission of the login form.
|
115
|
+
#
|
116
|
+
# Also there is no way to check the stay logged in option of No Kahuna yet.
|
117
|
+
# This is planned for a future version.
|
118
|
+
class User
|
119
|
+
attr_reader :projects, :authenticity_token, :agent
|
120
|
+
|
121
|
+
def initialize(user, pass)
|
122
|
+
@agent = Agent.new
|
123
|
+
|
124
|
+
@agent.post("#{BASE_URL}settings/use_locale?locale=en-US")
|
125
|
+
|
126
|
+
page = @agent.get("#{BASE_URL}login")
|
127
|
+
|
128
|
+
form = page.forms.first
|
129
|
+
@authenticity_token = form.authenticity_token
|
130
|
+
form.login = user
|
131
|
+
form.password = pass
|
132
|
+
|
133
|
+
page = @agent.submit(form)
|
134
|
+
|
135
|
+
@projects = SingleSelectArray.new
|
136
|
+
|
137
|
+
page.search('ul.projectList li a').each do |project_link|
|
138
|
+
name = project_link.children.last.clean
|
139
|
+
total_taks = project_link.search('span.taskCount span.total').first.clean
|
140
|
+
own_tasks = project_link.search('span.taskCount').first.children.first.clean.gsub(/^(\d+)\s\//, '\1').to_i
|
141
|
+
id = project_link.attributes['href'].gsub(/^#{PROJECT_URL}(\d+)\/.*$/, '\1')
|
142
|
+
|
143
|
+
@projects << Project.new(id, name, total_taks, own_tasks, self)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
# Representation of No Kahuna's projects.
|
151
|
+
#
|
152
|
+
# It holds all information about a project, like it's name and description,
|
153
|
+
# it's categories, members and tasks. It's also used to add new tasks to
|
154
|
+
# a project and can also be used to change the name and description of the
|
155
|
+
# project.
|
156
|
+
class Project
|
157
|
+
attr_reader :id, :agent, :user
|
158
|
+
attr_accessor :name, :total_tasks, :own_tasks, :description
|
159
|
+
|
160
|
+
def initialize(id, name, total_tasks, own_tasks, user)
|
161
|
+
@id = id
|
162
|
+
@name = name
|
163
|
+
@total_tasks_count = total_tasks
|
164
|
+
@own_tasks_count = own_tasks
|
165
|
+
|
166
|
+
@user = user
|
167
|
+
end
|
168
|
+
|
169
|
+
def description
|
170
|
+
unless @description
|
171
|
+
page = user.agent.get(url)
|
172
|
+
@description = page.search('div.projectDescription p').last.clean
|
173
|
+
end
|
174
|
+
@description
|
175
|
+
end
|
176
|
+
|
177
|
+
def description=(new_description)
|
178
|
+
@description = new_description
|
179
|
+
end
|
180
|
+
|
181
|
+
def categories
|
182
|
+
build_categories!(user.agent.get(url('tasks/new'))) unless @categories
|
183
|
+
|
184
|
+
@categories
|
185
|
+
end
|
186
|
+
|
187
|
+
def members
|
188
|
+
build_members!(user.agent.get(url('tasks/new'))) unless @members
|
189
|
+
|
190
|
+
@members
|
191
|
+
end
|
192
|
+
|
193
|
+
# The open tasks of the project
|
194
|
+
def tasks
|
195
|
+
build_tasks!(user.agent.get(url('tasks?group_by=category'))) unless @tasks
|
196
|
+
|
197
|
+
@tasks
|
198
|
+
end
|
199
|
+
|
200
|
+
# The closed tasks of the project
|
201
|
+
#
|
202
|
+
# For technical reasons I decided to devide the tasks into closed and open
|
203
|
+
# ones hoping that nobody really needs the closed ones ;)
|
204
|
+
# The problem is: At the moment the only chance to get an overview of closed
|
205
|
+
# tasks in the No Kahuna interface is to search for 'task'. But then
|
206
|
+
# you get no category information with the tasks. So I decided that it is
|
207
|
+
# more important to have the category information for any task available
|
208
|
+
# without the need to load a single page for any task which is possible
|
209
|
+
# with the normal task overview and which the tasks method does.
|
210
|
+
# If you really would like to see the closed tasks use this method by be
|
211
|
+
# aware, that if you access the category of a closed task a new HTTP request
|
212
|
+
# has to made!
|
213
|
+
#
|
214
|
+
# Also watch out for an other pitfall: If you close or reopen a task they
|
215
|
+
# stay in their original array! So if you do
|
216
|
+
# project.tasks.first.close!
|
217
|
+
# a call to
|
218
|
+
# project.tasks
|
219
|
+
# just after that would INCLUDE the closed task and if you already had
|
220
|
+
# called closed_tasks another call to that would NOT INCLUDE the closed
|
221
|
+
# task!
|
222
|
+
def closed_tasks
|
223
|
+
build_closed_tasks!(user.agent.get(url('tasks/search?s=task'))) unless @closed_tasks
|
224
|
+
|
225
|
+
@closed_tasks
|
226
|
+
end
|
227
|
+
|
228
|
+
# Adds a task to a project.
|
229
|
+
# The options hash can consist of the following keys:
|
230
|
+
# - :category => 'Some Category Name' OR some_category_object
|
231
|
+
# - :assign_to => 'Some User Name' OR some_member_object
|
232
|
+
# - :notify => 'Some User Name' OR some_member_object OR an array of them
|
233
|
+
# An example:
|
234
|
+
# project = Neoneo::User.new('John Doe', 'god').projects.find('My Project')
|
235
|
+
# project.add_task("A shiny new task",
|
236
|
+
# :assign_to => 'Bob Dillan',
|
237
|
+
# :category => project.categories.first,
|
238
|
+
# :notify => ['John Doe', project.members.last])
|
239
|
+
def add_task(description, options = {})
|
240
|
+
page = user.agent.get(url('tasks/new'))
|
241
|
+
|
242
|
+
build_categories!(page) unless @categories
|
243
|
+
build_members!(page) unless @members
|
244
|
+
|
245
|
+
category = categories.find_or_use(options[:category])
|
246
|
+
assign_to = members.find_or_use(options[:assign_to])
|
247
|
+
|
248
|
+
notifications = Array.new
|
249
|
+
case options[:notify]
|
250
|
+
when Array
|
251
|
+
options[:notify].each do |member|
|
252
|
+
notifications << members.find_or_use(member)
|
253
|
+
end
|
254
|
+
else
|
255
|
+
notifications << members.find_or_use(options[:notify])
|
256
|
+
end
|
257
|
+
notifications.compact!
|
258
|
+
|
259
|
+
page = user.agent.get(url('tasks/new'))
|
260
|
+
form = page.forms.last
|
261
|
+
|
262
|
+
form.send('task[body]'.to_sym, description)
|
263
|
+
form.send('task[assigned_to_id]'.to_sym, assign_to.id) if assign_to
|
264
|
+
form.send('task[category_id]'.to_sym, category.id) if category
|
265
|
+
|
266
|
+
notifications.each do |notification|
|
267
|
+
form.add_field!('subscriber_ids[]', notification.id)
|
268
|
+
end
|
269
|
+
|
270
|
+
user.agent.submit form
|
271
|
+
end
|
272
|
+
|
273
|
+
# Saves the project name and descriptions which you can set simply with
|
274
|
+
# name= and description=
|
275
|
+
# An example:
|
276
|
+
# project = Neoneo::User.new('John Doe', 'god').projects.find('My Project')
|
277
|
+
# project.name = 'BLA!'
|
278
|
+
# project.description = 'New description'
|
279
|
+
# project.save
|
280
|
+
def save
|
281
|
+
page = user.agent.get(url('edit'))
|
282
|
+
form = page.forms.last
|
283
|
+
form.send('project[name]='.to_sym, @name)
|
284
|
+
form.send('project[description]='.to_sym, @description) if @description
|
285
|
+
page = user.agent.submit form
|
286
|
+
|
287
|
+
raise Error unless page.search('div#flash.notice p').first.clean ==
|
288
|
+
'Successfully saved project'
|
289
|
+
end
|
290
|
+
|
291
|
+
# The URL to the project at No Kahuna
|
292
|
+
def url(appendix = '')
|
293
|
+
"#{PROJECT_URL}#{@id}/#{appendix}"
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def build_members!(page)
|
299
|
+
@members = SingleSelectArray.new
|
300
|
+
|
301
|
+
members = page.search('select#task_assigned_to_id option')
|
302
|
+
members.each do |member|
|
303
|
+
id = member.attributes['value']
|
304
|
+
@members << Member.new(id, member.innerText, self) unless id.empty?
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def build_categories!(page)
|
309
|
+
@categories = SingleSelectArray.new
|
310
|
+
|
311
|
+
categories = page.search('select#task_category_id option')
|
312
|
+
categories.each do |category|
|
313
|
+
id = category.attributes['value']
|
314
|
+
@categories << Category.new(id, category.innerText, self) unless id.empty?
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
def build_tasks!(page)
|
320
|
+
@tasks = SingleSelectArray.new
|
321
|
+
|
322
|
+
categories = page.search('div#task_list_grouped_by_category div.taskList')
|
323
|
+
|
324
|
+
categories.each do |category_div|
|
325
|
+
category = self.categories.find(category_div.search('h2').first.clean)
|
326
|
+
tasks = category_div.search('ul.sortable_tasks li')
|
327
|
+
tasks.each do |task_item|
|
328
|
+
user = Utils::URL.url_unescape(task_item.search('span.avatar a').first.attributes['href'].gsub(/^\/users\//, ''))
|
329
|
+
task_link = task_item.search('a.taskLink')
|
330
|
+
id = task_link.search('span.taskId').first.clean
|
331
|
+
description = task_link.search('span.taskShortBody').first.clean
|
332
|
+
@tasks << Task.new(id, description, category, members.find(user), self)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def build_closed_tasks!(page)
|
338
|
+
@closed_tasks = SingleSelectArray.new
|
339
|
+
|
340
|
+
tasks = page.search('div#tasks_for_me ul.search li.done')
|
341
|
+
|
342
|
+
tasks.each do |task_item|
|
343
|
+
user = Utils::URL.url_unescape(task_item.search('span.avatar a').first.attributes['href'].gsub(/^\/users\//, ''))
|
344
|
+
task_link = task_item.search('a.taskLink')
|
345
|
+
id = task_link.search('span.taskId').first.clean
|
346
|
+
description = task_link.search('span.taskShortBody').first.clean
|
347
|
+
@closed_tasks << Task.new(id, description, nil, members.find(user), self, true)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Representation of No Kahuna's categories for tasks
|
353
|
+
#
|
354
|
+
# At the moment it's a read-only class. So you can't change the name of a
|
355
|
+
# category and then save those change.
|
356
|
+
# This is planned for future versions.
|
357
|
+
class Category
|
358
|
+
attr_reader :id, :name
|
359
|
+
|
360
|
+
def initialize(id, name, project)
|
361
|
+
@id = id
|
362
|
+
@name = name
|
363
|
+
@project = project
|
364
|
+
end
|
365
|
+
|
366
|
+
# Adds a task to this category
|
367
|
+
#
|
368
|
+
# It works exactly as Project#add_task only with a pre-filled :category
|
369
|
+
# option. So please look there for further instructions on how to use it.
|
370
|
+
def add_task(description, options = {})
|
371
|
+
options[:category => self]
|
372
|
+
@project.add_task(description, options)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# Representation of No Kahuna users who are members of a project.
|
378
|
+
#
|
379
|
+
# This class is different to the User class! While a Neoneo::User describes
|
380
|
+
# a No Kahuna user from which you know the login credentials to No Kahuna
|
381
|
+
# a Neoneo::Member represents a No Kahuna user only with all the information
|
382
|
+
# you can get about it by sharing a project with your Neoneo::User.
|
383
|
+
# As you can imagine this is again a read-only class so no changes can be made
|
384
|
+
# (how should they, you don't know the member's password by definition!)
|
385
|
+
# And yes, even your Neoneo::User will be represented as a Neoneo::Member
|
386
|
+
# When you call Project#members. This is to avoid any confusion by dealing
|
387
|
+
# with two different classes with different abilities in one and the same
|
388
|
+
# array.
|
389
|
+
class Member
|
390
|
+
attr_reader :id, :name
|
391
|
+
|
392
|
+
def initialize(id, name, project)
|
393
|
+
@id = id
|
394
|
+
@name = name
|
395
|
+
@project = project
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# Represents a No Kahuna task
|
400
|
+
#
|
401
|
+
# At the moment this class is read-only in regards of it's description.
|
402
|
+
# But you can close or reopen a task.
|
403
|
+
# This will be improved as soon as possible!
|
404
|
+
class Task
|
405
|
+
attr_reader :id, :user, :project
|
406
|
+
|
407
|
+
def initialize(id, description, category, user, project, done = false)
|
408
|
+
@id = id
|
409
|
+
@description = description
|
410
|
+
@category = category
|
411
|
+
@user = user
|
412
|
+
@project = project
|
413
|
+
@done = done
|
414
|
+
|
415
|
+
@uncertain = @description =~ /\.{3}$/
|
416
|
+
end
|
417
|
+
|
418
|
+
def description
|
419
|
+
build_description! if @uncertain
|
420
|
+
@description
|
421
|
+
end
|
422
|
+
|
423
|
+
def category
|
424
|
+
build_category! unless @category
|
425
|
+
|
426
|
+
@category
|
427
|
+
end
|
428
|
+
|
429
|
+
def url(appendix = '')
|
430
|
+
@project.url("tasks/#{@id}/#{appendix}")
|
431
|
+
end
|
432
|
+
|
433
|
+
def close!
|
434
|
+
page = @project.user.agent.get(url)
|
435
|
+
form = page.forms.last
|
436
|
+
authenticity_token = form.authenticity_token
|
437
|
+
|
438
|
+
@project.user.agent.post(url('done'), :authenticity_token => authenticity_token, '_method'.to_sym => 'put')
|
439
|
+
@done = true
|
440
|
+
end
|
441
|
+
|
442
|
+
def reopen!
|
443
|
+
return unless @done
|
444
|
+
|
445
|
+
page = @project.user.agent.get(url)
|
446
|
+
form = page.forms.last
|
447
|
+
authenticity_token = form.authenticity_token
|
448
|
+
|
449
|
+
@project.user.agent.post(url('not_done'), :authenticity_token => authenticity_token, '_method'.to_sym => 'put')
|
450
|
+
@done = false
|
451
|
+
end
|
452
|
+
|
453
|
+
def closed?
|
454
|
+
@done
|
455
|
+
end
|
456
|
+
|
457
|
+
private
|
458
|
+
def build_description!(page = nil)
|
459
|
+
page ||= project.user.agent.get(url('edit'))
|
460
|
+
form = page.forms.last
|
461
|
+
@description = form.send('task[body]'.to_sym)
|
462
|
+
@uncertain = false
|
463
|
+
end
|
464
|
+
|
465
|
+
def build_category!(page = nil)
|
466
|
+
page ||= project.user.agent.get(url('edit'))
|
467
|
+
|
468
|
+
@category = project.categories.find(page.search('span.category').clear)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
end
|
data/spec/neoneo_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
data.tar.gz.sig
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: neoneo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Thorben Schr\xC3\xB6der"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDMjCCAhqgAwIBAgIBADANBgkqhkiG9w0BAQUFADA/MRAwDgYDVQQDDAd0aG9y
|
14
|
+
YmVuMRYwFAYKCZImiZPyLGQBGRYGZmV0bWFiMRMwEQYKCZImiZPyLGQBGRYDbmV0
|
15
|
+
MB4XDTA4MTAwOTIxNTQwNloXDTA5MTAwOTIxNTQwNlowPzEQMA4GA1UEAwwHdGhv
|
16
|
+
cmJlbjEWMBQGCgmSJomT8ixkARkWBmZldG1hYjETMBEGCgmSJomT8ixkARkWA25l
|
17
|
+
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOTrBWJAhXdd4FALdaz4
|
18
|
+
+2KKe6Loz7L8AQxvhYedX7trYpqrWmXNLyCZKvNDf7Hp0EmOn8k5Iti161bcWxwY
|
19
|
+
fj8ejQ02U3OUyKSQM7V7zUrzB9pkmZ8ROGWJmw+nWVu7ZF7UU6+kWwaSMU/unPno
|
20
|
+
c1PcfOQrwCjvXbedMTFPZ1b/W37DoaoVQJLzzx95ewXSZ7iPtLxTrjHESjWBPxFi
|
21
|
+
JMEVCZDM+5UTEm41ucAJJ58z54mKryRap4NMux9YmPFp13f0xFVKP5kST16Q96IV
|
22
|
+
qJaPKd4WApsB8WOOyxGVFzp6Lf1fAHKjrXca6ywHeAM070Ki6GzAXKPBzUV13/R7
|
23
|
+
azECAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFMCo
|
24
|
+
Q0fsO/qD4FD6zVoAIBw5ehlOMA0GCSqGSIb3DQEBBQUAA4IBAQCT+qucnHSHu9t0
|
25
|
+
Ntxpnm5gpQPVFz+kI6WCAqUeVlV5cbifH7/T+HKEePe+H3sF1eHG2X0QiXMYDZ26
|
26
|
+
Vgp6S9LCofXhJySOGYO26gcUyGfmkmQ//+YiwpJ0k+uznEM+RBNw/CSpFoXrnKa2
|
27
|
+
39/buzR3VtgPAcAOHb+5+WDIdX6NGgrKFF8udOqQ+rAvsoQXpJXfpfdqoFiOdfCa
|
28
|
+
Bqd6tQGVy0qUttoqMCOTxwMYWzoNs5GFXqtmbXxV6W2F81ipkELVVoWtSvRRkqtx
|
29
|
+
dX2CCcpgG+qXnji1CJyb6Dgm5ICJO/+B8ZKQ5qAYg798KOB7gyddzwRZWImtRoYU
|
30
|
+
kX4sVHCM
|
31
|
+
-----END CERTIFICATE-----
|
32
|
+
|
33
|
+
date: 2008-10-23 00:00:00 +02:00
|
34
|
+
default_executable:
|
35
|
+
dependencies:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: mechanize
|
38
|
+
type: :runtime
|
39
|
+
version_requirement:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 0.8.4
|
45
|
+
version:
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: hoe
|
48
|
+
type: :development
|
49
|
+
version_requirement:
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.8.0
|
55
|
+
version:
|
56
|
+
description: Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects.
|
57
|
+
email: thorben@fetmab.net
|
58
|
+
executables: []
|
59
|
+
|
60
|
+
extensions: []
|
61
|
+
|
62
|
+
extra_rdoc_files:
|
63
|
+
- History.txt
|
64
|
+
- Manifest.txt
|
65
|
+
- README.txt
|
66
|
+
files:
|
67
|
+
- History.txt
|
68
|
+
- Manifest.txt
|
69
|
+
- README.txt
|
70
|
+
- Rakefile
|
71
|
+
- lib/neoneo.rb
|
72
|
+
- spec/spec_helper.rb
|
73
|
+
- spec/neoneo_spec.rb
|
74
|
+
has_rdoc: true
|
75
|
+
homepage:
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options:
|
78
|
+
- --main
|
79
|
+
- README.txt
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
version:
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: "0"
|
93
|
+
version:
|
94
|
+
requirements: []
|
95
|
+
|
96
|
+
rubyforge_project: kickassrb
|
97
|
+
rubygems_version: 1.2.0
|
98
|
+
signing_key:
|
99
|
+
specification_version: 2
|
100
|
+
summary: Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects.
|
101
|
+
test_files: []
|
102
|
+
|
metadata.gz.sig
ADDED
Binary file
|