neoneo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|