taskwarrior-web 1.0.14 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,7 @@ rvm:
4
4
  - 1.9.3
5
5
  script: "rake spec"
6
6
  before_install:
7
- - sudo apt-get install task
7
+ - sudo apt-get update -qq
8
+ - sudo apt-get install task -qq
8
9
  - cp -v /home/vagrant/builds/theunraveler/taskwarrior-web/spec/files/taskrc /home/vagrant/.taskrc
9
10
  - mkdir /home/vagrant/.task
@@ -1,3 +1,10 @@
1
+ ## v1.1.0 (12/7/12)
2
+
3
+ * Editing and deleting tasks! (If you are using task >= 2.0).
4
+ * Fixing status messages. They should now be consistent.
5
+ * Added safeguards for xterm.title and color .taskrc settings. You should now
6
+ be able to safely set both of those options.
7
+
1
8
  ## v1.0.14 (11/22/12)
2
9
 
3
10
  * Fixed a bug when colorizing tasks based on "due" setting.
data/README.md CHANGED
@@ -13,10 +13,6 @@ forthcoming!**
13
13
 
14
14
  * `ruby` >= 1.9 (support for `ruby` < 1.9 is very unlikely, but pull requests
15
15
  are gladly accepted).
16
- * In your `.taskrc` file, `xterm.title` cannot be enabled. Either remove that
17
- line from `.taskrc` or set it to `off`. If you have a very compelling reason
18
- for needing this to be enabled, submit a bug report and I'll reconsider
19
- adding support for it.
20
16
 
21
17
  ## Installation
22
18
 
@@ -37,8 +33,9 @@ an executable, so all options for Vegas are valid for `task-web`. Type
37
33
 
38
34
  The current featureset includes:
39
35
 
40
- * Viewing tasks (duh) sorted and grouped in various ways.
36
+ * Viewing tasks sorted and grouped in various ways.
41
37
  * Creating a new task with a due date, project, and tags.
38
+ * Editing and deleting tasks (only task >= 2.0).
42
39
  * `task-web` will pull your `task` config (from `.taskrc`) and use it to
43
40
  determine date formatting and when an upcoming task should be marked as
44
41
  "due".
@@ -4,6 +4,7 @@ $:.unshift(File.dirname(__FILE__)) unless
4
4
  require 'rubygems'
5
5
  require 'active_support/core_ext/object/blank'
6
6
  require 'active_support/core_ext/string/inflections'
7
+ require 'active_support/core_ext/string/filters'
7
8
 
8
9
  module TaskwarriorWeb
9
10
  autoload :App, 'taskwarrior-web/app'
@@ -6,6 +6,7 @@ require 'time'
6
6
  require 'rinku'
7
7
  require 'digest'
8
8
  require 'sinatra/simple-navigation'
9
+ require 'rack-flash'
9
10
 
10
11
  class TaskwarriorWeb::App < Sinatra::Base
11
12
  autoload :Helpers, 'taskwarrior-web/helpers'
@@ -15,34 +16,25 @@ class TaskwarriorWeb::App < Sinatra::Base
15
16
  set :app_file, __FILE__
16
17
  set :public_folder, File.dirname(__FILE__) + '/public'
17
18
  set :views, File.dirname(__FILE__) + '/views'
19
+ set :method_override, true
20
+ enable :sessions
18
21
 
19
22
  # Helpers
20
23
  helpers Helpers
21
24
  register Sinatra::SimpleNavigation
25
+ use Rack::Flash
22
26
 
23
- def protected!
24
- response['WWW-Authenticate'] = %(Basic realm="Taskworrior Web") and throw(:halt, [401, "Not authorized\n"]) and return unless authorized?
25
- end
26
-
27
- def authorized?
28
- @auth ||= Rack::Auth::Basic::Request.new(request.env)
29
- values = [TaskwarriorWeb::Config.property('task-web.user'), TaskwarriorWeb::Config.property('task-web.passwd')]
30
- @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == values
31
- end
32
-
33
27
  # Before filter
34
28
  before do
35
29
  @current_page = request.path_info
30
+ @can_edit = TaskwarriorWeb::Config.supports? :editing
36
31
  protected! if TaskwarriorWeb::Config.property('task-web.user')
37
32
  end
38
33
 
39
34
  # Redirects
40
- get '/' do
41
- redirect '/tasks/pending'
42
- end
43
- get '/tasks/?' do
44
- redirect '/tasks/pending'
45
- end
35
+ get('/') { redirect to('/tasks/pending') }
36
+ get('/tasks/?') { redirect to('/tasks/pending') }
37
+ get('/projects/?') { redirect to('/projects/overview') }
46
38
 
47
39
  # Task routes
48
40
  get '/tasks/:status/?' do
@@ -60,26 +52,63 @@ class TaskwarriorWeb::App < Sinatra::Base
60
52
  get '/tasks/new/?' do
61
53
  @title = 'New Task'
62
54
  @date_format = (TaskwarriorWeb::Config.dateformat || 'm/d/yy').gsub('Y', 'yy')
63
- erb :task_form
55
+ erb :new_task
64
56
  end
65
57
 
66
58
  post '/tasks/?' do
67
59
  @task = TaskwarriorWeb::Task.new(params[:task])
68
60
 
69
61
  if @task.is_valid?
70
- @task.save!
71
- redirect '/tasks'
62
+ flash[:success] = @task.save! || %Q{New task "#{@task.description.truncate(20)}" created}
63
+ redirect to('/tasks')
72
64
  end
73
65
 
74
- @messages = @task._errors.map { |error| { :severity => 'alert-error', :message => error } }
75
- call! env.merge('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/tasks/new')
66
+ flash.now[:error] = @task._errors.join(', ')
67
+ forward '/tasks/new'
76
68
  end
77
69
 
78
- # Projects
79
- get '/projects' do
80
- redirect '/projects/overview'
70
+ get '/tasks/:uuid/?' do
71
+ not_found if !TaskwarriorWeb::Config.supports?(:editing)
72
+ tasks = TaskwarriorWeb::Task.find_by_uuid(params[:uuid])
73
+ not_found if tasks.empty?
74
+ @task = tasks.first
75
+ @title = %Q{Editing "#{@task.description.truncate(20)}"}
76
+ erb :edit_task
81
77
  end
82
78
 
79
+ patch '/tasks/:uuid/?' do
80
+ not_found if !TaskwarriorWeb::Config.supports?(:editing)
81
+ not_found if TaskwarriorWeb::Task.find_by_uuid(params[:uuid]).empty?
82
+
83
+ @task = TaskwarriorWeb::Task.new(params[:task])
84
+ if @task.is_valid?
85
+ flash[:success] = @task.save! || %Q{Task "#{@task.description.truncate(20)}" was successfully updated}
86
+ redirect to('/tasks')
87
+ end
88
+
89
+ flash.now[:error] = @task._errors.join(', ')
90
+ forward "/tasks/#{@task.uuid}"
91
+ end
92
+
93
+ get '/tasks/:uuid/delete/?' do
94
+ not_found if !TaskwarriorWeb::Config.supports?(:editing)
95
+ tasks = TaskwarriorWeb::Task.find_by_uuid(params[:uuid])
96
+ not_found if tasks.empty?
97
+ @task = tasks.first
98
+ @title = %Q{Are you sure you want to delete the task "#{@task.description.truncate(20)}"?}
99
+ erb :delete_confirm
100
+ end
101
+
102
+ delete '/tasks/:uuid' do
103
+ not_found if !TaskwarriorWeb::Config.supports?(:editing)
104
+ tasks = TaskwarriorWeb::Task.find_by_uuid(params[:uuid])
105
+ not_found if tasks.empty?
106
+ @task = tasks.first
107
+ flash[:success] = @task.delete! || %Q{The task "#{@task.description.truncate(20)}" was successfully deleted}
108
+ redirect to('/tasks')
109
+ end
110
+
111
+ # Projects
83
112
  get '/projects/overview/?' do
84
113
  @title = 'Projects'
85
114
  @tasks = TaskwarriorWeb::Task.query('status.not' => :deleted, 'project.not' => '')
@@ -90,49 +119,30 @@ class TaskwarriorWeb::App < Sinatra::Base
90
119
  end
91
120
 
92
121
  get '/projects/:name/?' do
93
- @title = params[:name].gsub('--', '.')
122
+ @title = unlinkify(params[:name])
94
123
  @tasks = TaskwarriorWeb::Task.query('status.not' => 'deleted', :project => @title)
95
124
  .sort_by! { |x| [x.priority.nil?.to_s, x.priority.to_s, x.due.nil?.to_s, x.due.to_s] }
96
125
  erb :project
97
126
  end
98
127
 
99
128
  # AJAX callbacks
100
- get '/ajax/projects/?' do
101
- TaskwarriorWeb::Command.new(:projects).run.split("\n").to_json
102
- end
103
-
104
- get '/ajax/count/?' do
105
- self.class.task_count
106
- end
129
+ get('/ajax/projects/?') { TaskwarriorWeb::Command.new(:projects).run.split("\n").to_json }
130
+ get('/ajax/count/?') { task_count }
131
+ post('/ajax/task-complete/:id/?') { TaskwarriorWeb::Command.new(:complete, params[:id]).run }
107
132
 
108
- get '/ajax/badge-count/?' do
133
+ get '/ajax/badge/?' do
109
134
  if filter = TaskwarriorWeb::Config.property('task-web.filter.badge')
110
135
  total = TaskwarriorWeb::Task.query(:description => filter).count
111
136
  else
112
- total = self.class.task_count
137
+ total = task_count
113
138
  end
114
139
  total == 0 ? '' : total.to_s
115
140
  end
116
141
 
117
- post '/ajax/task-complete/:id/?' do
118
- # Bummer that we have to directly use Command here, but apparently tasks
119
- # cannot be filtered by UUID.
120
- TaskwarriorWeb::Command.new(:complete, params[:id]).run
121
- end
122
-
123
142
  # Error handling
124
143
  not_found do
125
144
  @title = 'Page Not Found'
126
145
  @referrer = request.referrer
127
146
  erb :'404'
128
147
  end
129
-
130
- def self.task_count
131
- if filter = TaskwarriorWeb::Config.property('task-web.filter')
132
- total = TaskwarriorWeb::Task.query(:description => filter).count
133
- else
134
- total = TaskwarriorWeb::Task.count(:status => :pending)
135
- end
136
- total.to_s
137
- end
138
148
  end
@@ -5,7 +5,7 @@ SimpleNavigation::Configuration.run do |navigation|
5
5
  primary.dom_class = 'nav'
6
6
  primary.item :tasks, 'Tasks', '/tasks' do |tasks|
7
7
  tasks.dom_class = 'nav nav-pills'
8
- tasks.item :pending, "Pending <span class=\"badge\">#{TaskwarriorWeb::App.task_count}</span>", '/tasks/pending'
8
+ tasks.item :pending, "Pending <span class=\"badge\">#{task_count}</span>", '/tasks/pending'
9
9
  tasks.item :waiting, 'Waiting', '/tasks/waiting'
10
10
  tasks.item :completed, 'Completed', '/tasks/completed'
11
11
  tasks.item :deleted, 'Deleted', '/tasks/deleted'
@@ -1,7 +1,6 @@
1
1
  require 'active_support/core_ext/date/calculations'
2
2
 
3
3
  module TaskwarriorWeb::App::Helpers
4
-
5
4
  def format_date(timestamp)
6
5
  format = TaskwarriorWeb::Config.dateformat || '%-m/%-d/%Y'
7
6
  Time.parse(timestamp).strftime(format)
@@ -20,11 +19,49 @@ module TaskwarriorWeb::App::Helpers
20
19
  end
21
20
 
22
21
  def linkify(item)
23
- return if item.nil?
24
- item.gsub('.', '--')
22
+ item.gsub('.', '--') unless item.nil? unless item.nil?
23
+ end
24
+
25
+ def unlinkify(item)
26
+ item.gsub('--', '.') unless item.nil?
25
27
  end
26
28
 
27
29
  def auto_link(text)
28
30
  Rinku.auto_link(text, :all, 'target="_blank"')
29
31
  end
32
+
33
+ def flash_types
34
+ [:success, :info, :warning, :error]
35
+ end
36
+
37
+ def task_count
38
+ if filter = TaskwarriorWeb::Config.property('task-web.filter')
39
+ total = TaskwarriorWeb::Task.query(:description => filter).count
40
+ else
41
+ total = TaskwarriorWeb::Task.count(:status => :pending)
42
+ end
43
+ total.to_s
44
+ end
45
+
46
+ def crud_links(task)
47
+ string = %Q{<a href="/tasks/#{task.uuid}">Edit</a>}
48
+ string << %Q{&nbsp;|&nbsp;}
49
+ string << %Q{<a href="/tasks/#{task.uuid}/delete">Delete</a>}
50
+ string
51
+ end
52
+
53
+ # Authentication
54
+ def protected!
55
+ response['WWW-Authenticate'] = %(Basic realm="Taskworrior Web") and throw(:halt, [401, "Not authorized\n"]) and return unless authorized?
56
+ end
57
+
58
+ def authorized?
59
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
60
+ values = [TaskwarriorWeb::Config.property('task-web.user'), TaskwarriorWeb::Config.property('task-web.passwd')]
61
+ @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == values
62
+ end
63
+
64
+ def forward(url, method = 'GET')
65
+ call env.merge('REQUEST_METHOD' => method, 'PATH_INFO' => url)
66
+ end
30
67
  end
@@ -22,7 +22,7 @@ module TaskwarriorWeb::Config
22
22
  }
23
23
 
24
24
  def self.version
25
- @version ||= Versionomy.parse(`task _version`.strip)
25
+ @version ||= Versionomy.parse(`#{TaskwarriorWeb::Runner::TASK_BIN} _version`.strip)
26
26
  end
27
27
 
28
28
  def self.file
@@ -37,6 +37,13 @@ module TaskwarriorWeb::Config
37
37
  self.file['dateformat'].gsub(/(\w)/, DATEFORMATS) unless self.file['dateformat'].nil?
38
38
  end
39
39
 
40
+ def self.supports?(feature)
41
+ case feature.to_sym
42
+ when :editing then self.version.major > 1
43
+ else false
44
+ end
45
+ end
46
+
40
47
  def self.method_missing(method)
41
48
  self.file[method.to_s]
42
49
  end
@@ -7,7 +7,7 @@ module TaskwarriorWeb
7
7
 
8
8
  attr_accessor :entry, :project, :priority, :uuid, :description, :status,
9
9
  :due, :start, :end, :tags, :depends, :wait, :annotations,
10
- :_errors
10
+ :_errors, :remove_tags
11
11
  alias :annotate= :annotations=
12
12
 
13
13
  ####################################
@@ -24,16 +24,24 @@ module TaskwarriorWeb
24
24
  end
25
25
 
26
26
  def save!
27
- Command.new(:add, nil, self.to_hash).run
27
+ @uuid ? Command.new(:update, uuid, self.to_hash).run : Command.new(:add, nil, self.to_hash).run
28
28
  end
29
29
 
30
30
  def complete!
31
31
  Command.new(:complete, self.uuid).run
32
32
  end
33
33
 
34
+ def delete!
35
+ Command.new(:delete, self.uuid).run
36
+ end
37
+
34
38
  # Make sure that the tags are an array.
35
39
  def tags=(value)
36
40
  @tags = value.is_a?(String) ? value.split(/[, ]+/).reject(&:empty?) : value
41
+
42
+ if @uuid
43
+ @remove_tags = Task.find_by_uuid(uuid).first.tags - @tags
44
+ end
37
45
  end
38
46
 
39
47
  def is_valid?
@@ -71,7 +71,7 @@ var initTaskCompletion = function() {
71
71
 
72
72
  var refreshDockBadge = function() {
73
73
  if (window.hasOwnProperty('fluid')) {
74
- $.get('/ajax/badge-count', function(data) {
74
+ $.get('/ajax/badge', function(data) {
75
75
  window.fluid.dockBadge = data;
76
76
  });
77
77
  }
@@ -90,5 +90,7 @@ var refreshSubnavCount = function() {
90
90
  * @param string [severity] The severity of the message.
91
91
  */
92
92
  function set_message(msg, severity) {
93
- $('#flash-messages').append('<div class="alert alert-' + (severity || 'success') + '">' + msg + '</div>');
93
+ $('<div style="display: none;" class="alert alert-' + (severity || 'success') + '">' + msg + '</div>')
94
+ .appendTo('#flash-messages')
95
+ .fadeIn();
94
96
  }
@@ -4,6 +4,8 @@ module TaskwarriorWeb::CommandBuilder::Base
4
4
 
5
5
  TASK_COMMANDS = {
6
6
  :add => 'add',
7
+ :update => TaskwarriorWeb::Config.version.major >= 2 ? ':id mod' : nil,
8
+ :delete => 'rc.confirmation=no :id delete',
7
9
  :query => TaskwarriorWeb::Config.version > Versionomy.parse('1.9.2') ? '_query' : 'export',
8
10
  :complete => ':id done',
9
11
  :projects => '_projects',
@@ -20,7 +22,7 @@ module TaskwarriorWeb::CommandBuilder::Base
20
22
  end
21
23
 
22
24
  def task_command
23
- if TASK_COMMANDS.has_key?(@command.to_sym)
25
+ if TASK_COMMANDS[@command.to_sym]
24
26
  @command_string = TASK_COMMANDS[@command.to_sym].clone
25
27
  return self
26
28
  else
@@ -45,6 +47,10 @@ module TaskwarriorWeb::CommandBuilder::Base
45
47
  tags.each { |tag| string << %Q( #{tag_indicator}#{tag.to_s.shellescape}) }
46
48
  end
47
49
 
50
+ if tags = @params.delete(:remove_tags)
51
+ tags.each { |tag| string << %Q( -#{tag.to_s.shellescape}) }
52
+ end
53
+
48
54
  @params.each do |attr, value|
49
55
  if value.respond_to? :each
50
56
  value.each { |val| string << %Q( #{attr.to_s}:\\"#{val.to_s.shellescape}\\") }
@@ -1,5 +1,5 @@
1
1
  module TaskwarriorWeb::Runner
2
- TASK_BIN = 'task'
2
+ TASK_BIN = 'task rc.xterm.title=no rc.color=off rc.verbose=no'
3
3
 
4
4
  def run
5
5
  @built ||= build
@@ -0,0 +1,7 @@
1
+ <div id="flash-messages">
2
+ <% flash_types.select{ |severity| flash.has?(severity) }.each do |severity| %>
3
+ <div class="alert alert-<%= severity %>">
4
+ <%= flash[severity] %>
5
+ </div>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,28 @@
1
+ <div class="control-group">
2
+ <label for="task-description" class="control-label">Description</label>
3
+ <div class="controls">
4
+ <input type="textfield" id="task-description" name="task[description]" value="<%= @task.description unless @task.nil? %>" />
5
+ </div>
6
+ </div>
7
+
8
+ <div class="control-group">
9
+ <label for="task-project" class="control-label">Project</label>
10
+ <div class="controls">
11
+ <input type="textfield" id="task-project" name="task[project]" value="<%= @task.project unless @task.nil? %>" autocomplete="off" />
12
+ </div>
13
+ </div>
14
+
15
+ <div class="control-group">
16
+ <label for="task-due" class="control-label">Due Date</label>
17
+ <div class="controls">
18
+ <input class="date-picker" type="textfield" id="task-due" name="task[due]" value="<%= format_date(@task.due) unless @task.nil? || @task.due.blank? %>" data-date-format="<%= @date_format %>" />
19
+ </div>
20
+ </div>
21
+
22
+ <div class="control-group">
23
+ <label for="task-tags" class="control-label">Tags</label>
24
+ <div class="controls">
25
+ <input type="textfield" id="task-tags" name="task[tags]" value="<%= @task.tags.join(', ') unless @task.nil? %>" autocomplete="off" />
26
+ <span class="help-block">Enter tags separated by commas or spaces (e.g. <em>each, word will,be a tag</em>)</span>
27
+ </div>
28
+ </div>
@@ -0,0 +1,4 @@
1
+ <form action="/tasks/<%= @task.uuid %>" method="post">
2
+ <input type="hidden" name="_method" value="delete" />
3
+ <input type="submit" class="btn btn-danger" value="Delete" /> or <a href="<%= request.referrer %>">cancel</a>
4
+ </form>
@@ -0,0 +1,13 @@
1
+ <form id="new-task-form" class="form-horizontal" action="/tasks/<%= @task.uuid %>" method="post">
2
+ <input type="hidden" name="task[uuid]" value="<%= @task.uuid %>" />
3
+
4
+ <%= erb :_task_form %>
5
+
6
+ <div class="control-group">
7
+ <div class="controls">
8
+ <button type="submit" class="btn btn-primary">Update Task</button>
9
+ <input type="hidden" name="_method" value="patch" />
10
+ </div>
11
+ </div>
12
+
13
+ </form>
@@ -19,13 +19,7 @@
19
19
  <%= erb :_topbar %>
20
20
  <%= erb :_subnav %>
21
21
  <section class="content">
22
- <div id="flash-messages">
23
- <% if @messages %>
24
- <% @messages.each do |message| %>
25
- <div class="alert <%= message[:severity] %>"><%= message[:message] %></div>
26
- <% end %>
27
- <% end %>
28
- </div>
22
+ <%= erb :_flash %>
29
23
  <%= yield %>
30
24
  </section>
31
25
  </div>
@@ -1,3 +1,5 @@
1
+ <% can_edit = @can_edit && params[:status] == 'pending' %>
2
+
1
3
  <div id="listing">
2
4
  <table class="table table-striped table-hover">
3
5
  <thead>
@@ -10,6 +12,7 @@
10
12
  <th>Due</th>
11
13
  <th>Tags</th>
12
14
  <th>Priority</th>
15
+ <% if @can_edit %><th></th><% end %>
13
16
  </tr>
14
17
  </thead>
15
18
  <tbody>
@@ -36,6 +39,7 @@
36
39
  <td><%= format_date(task.due) unless task.due.nil? %></td>
37
40
  <td><%= task.tags.join(', ') unless task.tags.nil? %></td>
38
41
  <td><%= task.priority unless task.priority.nil? %></td>
42
+ <% if can_edit %><td><%= crud_links(task) %></td><% end %>
39
43
  </tr>
40
44
  <% end %>
41
45
  </tbody>
@@ -0,0 +1,11 @@
1
+ <form id="new-task-form" class="form-horizontal" action="/tasks" method="post">
2
+
3
+ <%= erb :_task_form %>
4
+
5
+ <div class="control-group">
6
+ <div class="controls">
7
+ <button type="submit" class="btn btn-primary">Create Task</button>
8
+ </div>
9
+ </div>
10
+
11
+ </form>
@@ -6,6 +6,7 @@
6
6
  <th>Due</th>
7
7
  <th>Tags</th>
8
8
  <th>Priority</th>
9
+ <% if @can_edit %><th></th><% end %>
9
10
  </tr>
10
11
  </thead>
11
12
  <tbody>
@@ -16,6 +17,7 @@
16
17
  <td><%= format_date(task.due) unless task.due.nil? %></td>
17
18
  <td><%= task.tags.join(', ') unless task.tags.nil? %></td>
18
19
  <td><%= task.priority unless task.priority.nil? %></td>
20
+ <% if @can_edit %><td><%= crud_links(task) %></td><% end %>
19
21
  </tr>
20
22
  <% end %>
21
23
  <% end %>
@@ -28,6 +28,7 @@
28
28
  <th>Due</th>
29
29
  <th>Tags</th>
30
30
  <th>Priority</th>
31
+ <% if @can_edit %><th></th><% end %>
31
32
  </tr>
32
33
  </thead>
33
34
  <tbody>
@@ -38,6 +39,7 @@
38
39
  <td><%= format_date(task.due) unless task.due.nil? %></td>
39
40
  <td><%= task.tags.join(', ') unless task.tags.nil? %></td>
40
41
  <td><%= task.priority unless task.priority.nil? %></td>
42
+ <% if @can_edit %><td><%= crud_links(task) %></td><% end %>
41
43
  </tr>
42
44
  <% end %>
43
45
  <% end %>
@@ -22,26 +22,16 @@ describe TaskwarriorWeb::App do
22
22
  get path
23
23
  follow_redirect!
24
24
 
25
- last_request.url.should =~ /tasks\/pending/
25
+ last_request.url.should match(/tasks\/pending$/)
26
26
  last_response.should be_ok
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
- describe 'GET /projects' do
32
- it 'should redirect to /projects/overview' do
33
- get '/projects'
34
- follow_redirect!
35
-
36
- last_request.url.should =~ /projects\/overview/
37
- last_response.should be_ok
38
- end
39
- end
40
-
41
31
  describe 'GET /tasks/new' do
42
32
  it 'should display a new task form' do
43
33
  get '/tasks/new'
44
- last_response.body.should =~ /form/
34
+ last_response.body.should include('<form')
45
35
  end
46
36
 
47
37
  it 'should display a 200 status code' do
@@ -60,13 +50,13 @@ describe TaskwarriorWeb::App do
60
50
  end
61
51
 
62
52
  it 'should redirect to the task listing page' do
63
- task = TaskwarriorWeb::Task.new
53
+ task = TaskwarriorWeb::Task.new({:description => 'Test task'})
64
54
  task.should_receive(:is_valid?).and_return(true)
65
55
  task.should_receive(:save!)
66
56
  TaskwarriorWeb::Task.should_receive(:new).once.and_return(task)
67
- post '/tasks', :task => {}
57
+ post '/tasks', :task => {:description => 'Test task'}
68
58
  follow_redirect!
69
- last_request.url.should =~ /\/tasks$/
59
+ last_request.url.should match(/tasks$/)
70
60
  end
71
61
  end
72
62
 
@@ -82,17 +72,134 @@ describe TaskwarriorWeb::App do
82
72
  task = TaskwarriorWeb::Task.new({:tags => 'tag1, tag2'})
83
73
  TaskwarriorWeb::Task.should_receive(:new).once.and_return(task)
84
74
  post '/tasks', :task => {}
85
- last_response.body.should =~ /form/
86
- last_response.body.should =~ /tag1, tag2/
75
+ last_response.body.should include('form')
76
+ last_response.body.should include('tag1, tag2')
87
77
  end
88
78
 
89
79
  it 'should display errors messages' do
90
80
  task = TaskwarriorWeb::Task.new
91
81
  TaskwarriorWeb::Task.should_receive(:new).once.and_return(task)
92
82
  post '/tasks', :task => {}
93
- last_response.body.should =~ /You must provide a description/
83
+ last_response.body.should include('You must provide a description')
84
+ end
85
+ end
86
+ end
87
+
88
+ describe 'GET /tasks/:uuid' do
89
+ context 'given a non-existant task' do
90
+ it 'should return a 404' do
91
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([])
92
+ get '/tasks/1'
93
+ last_response.should be_not_found
94
94
  end
95
95
  end
96
+
97
+ context 'given an existing task' do
98
+ before do
99
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([
100
+ TaskwarriorWeb::Task.new({:uuid => 246, :description => 'Test task with a longer description'})
101
+ ])
102
+ get '/tasks/246'
103
+ end
104
+
105
+ it 'should render an edit form' do
106
+ last_response.body.should have_tag('form', :with => { :action => '/tasks/246', :method => 'post' }) do
107
+ with_tag('input', :with => { :name => '_method', :value => 'patch' })
108
+ end
109
+ end
110
+
111
+ it 'should set the HTTP method in the form' do
112
+ end
113
+
114
+ it 'should truncate the task description' do
115
+ last_response.body.should have_tag('title', :text => /Test task with a .../)
116
+ end
117
+
118
+ it 'should fill the form fields with existing data' do
119
+ last_response.body.should have_tag('input', :with => { :name => 'task[description]', :value => 'Test task with a longer description' })
120
+ end
121
+ end
122
+ end
123
+
124
+ describe 'PATCH /tasks/:uuid' do
125
+ context 'given a non-existant task' do
126
+ it 'should return a 404' do
127
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([])
128
+ patch '/tasks/429897527'
129
+ last_response.should be_not_found
130
+ end
131
+ end
132
+ end
133
+
134
+ describe 'GET /tasks/:uuid/delete' do
135
+ context 'given a non-existant task' do
136
+ it 'should return a 404' do
137
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([])
138
+ get '/tasks/429897527/delete'
139
+ last_response.should be_not_found
140
+ end
141
+ end
142
+
143
+ context 'given an existing task' do
144
+ before do
145
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([
146
+ TaskwarriorWeb::Task.new({:uuid => 246, :description => 'Test task with a longer description'})
147
+ ])
148
+ get '/tasks/246/delete'
149
+ end
150
+
151
+ it 'should show a delete form' do
152
+ last_response.body.should have_tag('form', :with => { :action => '/tasks/246', :method => 'post' }) do
153
+ with_tag('input', :with => { :name => '_method', :value => 'delete' })
154
+ end
155
+ end
156
+
157
+ it 'should display a delete button' do
158
+ last_response.body.should have_tag('input', :with => { :type => 'submit', :value => 'Delete' })
159
+ end
160
+ end
161
+ end
162
+
163
+ describe 'DELETE /tasks/:uuid' do
164
+ context 'given a non-existant task' do
165
+ it 'should return a 404' do
166
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([])
167
+ delete '/tasks/429897527'
168
+ last_response.should be_not_found
169
+ end
170
+ end
171
+
172
+ context 'given an existing task' do
173
+ before do
174
+ @task = TaskwarriorWeb::Task.new
175
+ @task.should_receive(:delete!).once.and_return('Success')
176
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([@task])
177
+ delete '/tasks/246'
178
+ end
179
+
180
+ it 'should redirect to the task overview page' do
181
+ follow_redirect!
182
+ last_request.url.should match(/tasks$/)
183
+ end
184
+ end
185
+ end
186
+
187
+ describe 'GET /projects' do
188
+ it 'should redirect to /projects/overview' do
189
+ get '/projects'
190
+ follow_redirect!
191
+
192
+ last_request.url.should match(/projects\/overview$/)
193
+ last_response.should be_ok
194
+ end
195
+ end
196
+
197
+ describe 'GET /projects/:name' do
198
+ it 'should replace characters in the title' do
199
+ TaskwarriorWeb::Task.should_receive(:query).any_number_of_times.and_return([])
200
+ get '/projects/Test--Project'
201
+ last_response.body.should include('<title>Test.Project')
202
+ end
96
203
  end
97
204
 
98
205
  describe 'GET /ajax/projects' do
@@ -126,12 +233,12 @@ describe TaskwarriorWeb::App do
126
233
  describe 'not_found' do
127
234
  it 'should set the title to "Not Found"' do
128
235
  get '/page-not-found'
129
- last_response.body.should =~ /<title>Page Not Found/
236
+ last_response.body.should include('<title>Page Not Found')
130
237
  end
131
238
 
132
239
  it 'should have a status code of 404' do
133
240
  get '/page-not-found'
134
- last_response.status.should eq(404)
241
+ last_response.should be_not_found
135
242
  end
136
243
  end
137
244
  end
@@ -38,6 +38,16 @@ describe TaskwarriorWeb::Task do
38
38
  end
39
39
  end
40
40
 
41
+ describe '#delete!' do
42
+ it 'should delete the task' do
43
+ task = TaskwarriorWeb::Task.new({:uuid => 15})
44
+ command = TaskwarriorWeb::Command.new(:delete)
45
+ command.should_receive(:run).once
46
+ TaskwarriorWeb::Command.should_receive(:new).once.with(:delete, 15).and_return(command)
47
+ task.delete!
48
+ end
49
+ end
50
+
41
51
  describe '.query' do
42
52
  before do
43
53
  @command = TaskwarriorWeb::Command.new(:query)
@@ -82,6 +92,16 @@ describe TaskwarriorWeb::Task do
82
92
  task = TaskwarriorWeb::Task.new(:tags => '@hi, -twice, !again, ~when')
83
93
  task.tags.should eq(['@hi', '-twice', '!again', '~when'])
84
94
  end
95
+
96
+ it 'should properly set tags for removal' do
97
+ task = TaskwarriorWeb::Task.new({:tags => ['hello', 'goodbye']})
98
+ TaskwarriorWeb::Task.should_receive(:find_by_uuid).and_return([task])
99
+ task2 = TaskwarriorWeb::Task.new
100
+ task2.uuid = 15
101
+ task2.tags = ['goodbye']
102
+ task2.tags.should eq(['goodbye'])
103
+ task2.remove_tags.should eq(['hello'])
104
+ end
85
105
  end
86
106
 
87
107
  describe '#to_hash' do
@@ -51,6 +51,12 @@ describe TaskwarriorWeb::CommandBuilder::Base do
51
51
  command.params.should eq(' +today +tomorrow')
52
52
  end
53
53
 
54
+ it 'should remove tags using a -' do
55
+ command = TaskwarriorWeb::Command.new(:add, nil, :remove_tags => [:test, :tag])
56
+ command.parse_params
57
+ command.params.should eq(' -test -tag')
58
+ end
59
+
54
60
  it 'should pull out the description parameter' do
55
61
  command = TaskwarriorWeb::Command.new(:add, nil, :description => 'Hello', :status => :pending)
56
62
  command.parse_params
@@ -15,7 +15,7 @@ describe TaskwarriorWeb::Runner do
15
15
  end
16
16
 
17
17
  it 'should add a TASK_VERSION constant' do
18
- @object.class.const_get(:TASK_BIN).should eq('task')
18
+ @object.class.const_defined?(:TASK_BIN).should be_true
19
19
  end
20
20
  end
21
21
 
@@ -2,6 +2,7 @@ require 'sinatra'
2
2
  require 'rack/test'
3
3
  require 'rspec'
4
4
  require 'simple_navigation'
5
+ require 'rspec-html-matchers'
5
6
 
6
7
  # Simplecov
7
8
  require 'simplecov'
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "taskwarrior-web"
6
- s.version = '1.0.14'
6
+ s.version = '1.1.0'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Jake Bell"]
9
9
  s.email = ["jake@theunraveler.com"]
@@ -16,17 +16,18 @@ Gem::Specification.new do |s|
16
16
  s.required_ruby_version = '>= 1.9.0'
17
17
 
18
18
  s.add_dependency('sinatra')
19
- s.add_dependency('thin')
20
19
  s.add_dependency('parseconfig')
21
20
  s.add_dependency('vegas')
22
21
  s.add_dependency('rinku')
23
22
  s.add_dependency('versionomy')
24
23
  s.add_dependency('activesupport')
25
24
  s.add_dependency('sinatra-simple-navigation')
25
+ s.add_dependency('rack-flash3')
26
26
 
27
27
  s.add_development_dependency('rake')
28
28
  s.add_development_dependency('rack-test')
29
29
  s.add_development_dependency('rspec')
30
+ s.add_development_dependency('rspec-html-matchers')
30
31
 
31
32
  s.files = `git ls-files`.split("\n")
32
33
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taskwarrior-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.14
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-22 00:00:00.000000000 Z
12
+ date: 2012-12-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
16
- requirement: &70303595592080 !ruby/object:Gem::Requirement
16
+ requirement: &70274029036160 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70303595592080
24
+ version_requirements: *70274029036160
25
25
  - !ruby/object:Gem::Dependency
26
- name: thin
27
- requirement: &70303595590940 !ruby/object:Gem::Requirement
26
+ name: parseconfig
27
+ requirement: &70274029035440 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70303595590940
35
+ version_requirements: *70274029035440
36
36
  - !ruby/object:Gem::Dependency
37
- name: parseconfig
38
- requirement: &70303595590320 !ruby/object:Gem::Requirement
37
+ name: vegas
38
+ requirement: &70274029034480 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70303595590320
46
+ version_requirements: *70274029034480
47
47
  - !ruby/object:Gem::Dependency
48
- name: vegas
49
- requirement: &70303595589220 !ruby/object:Gem::Requirement
48
+ name: rinku
49
+ requirement: &70274029033940 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *70303595589220
57
+ version_requirements: *70274029033940
58
58
  - !ruby/object:Gem::Dependency
59
- name: rinku
60
- requirement: &70303595588600 !ruby/object:Gem::Requirement
59
+ name: versionomy
60
+ requirement: &70274029032960 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *70303595588600
68
+ version_requirements: *70274029032960
69
69
  - !ruby/object:Gem::Dependency
70
- name: versionomy
71
- requirement: &70303595587840 !ruby/object:Gem::Requirement
70
+ name: activesupport
71
+ requirement: &70274029031920 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *70303595587840
79
+ version_requirements: *70274029031920
80
80
  - !ruby/object:Gem::Dependency
81
- name: activesupport
82
- requirement: &70303595587380 !ruby/object:Gem::Requirement
81
+ name: sinatra-simple-navigation
82
+ requirement: &70274029031060 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :runtime
89
89
  prerelease: false
90
- version_requirements: *70303595587380
90
+ version_requirements: *70274029031060
91
91
  - !ruby/object:Gem::Dependency
92
- name: sinatra-simple-navigation
93
- requirement: &70303595586840 !ruby/object:Gem::Requirement
92
+ name: rack-flash3
93
+ requirement: &70274029030180 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :runtime
100
100
  prerelease: false
101
- version_requirements: *70303595586840
101
+ version_requirements: *70274029030180
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: rake
104
- requirement: &70303595580440 !ruby/object:Gem::Requirement
104
+ requirement: &70274029029420 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ! '>='
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: '0'
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *70303595580440
112
+ version_requirements: *70274029029420
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: rack-test
115
- requirement: &70303595579780 !ruby/object:Gem::Requirement
115
+ requirement: &70274029028620 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ! '>='
@@ -120,10 +120,21 @@ dependencies:
120
120
  version: '0'
121
121
  type: :development
122
122
  prerelease: false
123
- version_requirements: *70303595579780
123
+ version_requirements: *70274029028620
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: rspec
126
- requirement: &70303595579240 !ruby/object:Gem::Requirement
126
+ requirement: &70274029027800 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: *70274029027800
135
+ - !ruby/object:Gem::Dependency
136
+ name: rspec-html-matchers
137
+ requirement: &70274029027120 !ruby/object:Gem::Requirement
127
138
  none: false
128
139
  requirements:
129
140
  - - ! '>='
@@ -131,7 +142,7 @@ dependencies:
131
142
  version: '0'
132
143
  type: :development
133
144
  prerelease: false
134
- version_requirements: *70303595579240
145
+ version_requirements: *70274029027120
135
146
  description: This gem provides a graphical frontend for the Taskwarrior task manager.
136
147
  It is based on Sinatra.
137
148
  email:
@@ -179,13 +190,17 @@ files:
179
190
  - lib/taskwarrior-web/services/parser/json.rb
180
191
  - lib/taskwarrior-web/services/runner.rb
181
192
  - lib/taskwarrior-web/views/404.erb
193
+ - lib/taskwarrior-web/views/_flash.erb
182
194
  - lib/taskwarrior-web/views/_subnav.erb
195
+ - lib/taskwarrior-web/views/_task_form.erb
183
196
  - lib/taskwarrior-web/views/_topbar.erb
197
+ - lib/taskwarrior-web/views/delete_confirm.erb
198
+ - lib/taskwarrior-web/views/edit_task.erb
184
199
  - lib/taskwarrior-web/views/layout.erb
185
200
  - lib/taskwarrior-web/views/listing.erb
201
+ - lib/taskwarrior-web/views/new_task.erb
186
202
  - lib/taskwarrior-web/views/project.erb
187
203
  - lib/taskwarrior-web/views/projects.erb
188
- - lib/taskwarrior-web/views/task_form.erb
189
204
  - spec/app/app_spec.rb
190
205
  - spec/app/helpers_spec.rb
191
206
  - spec/files/taskrc
@@ -219,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
219
234
  version: '0'
220
235
  segments:
221
236
  - 0
222
- hash: -2980071334250491530
237
+ hash: 649655695520079499
223
238
  requirements: []
224
239
  rubyforge_project: taskwarrior-web
225
240
  rubygems_version: 1.8.11
@@ -1,38 +0,0 @@
1
- <form id="new-task-form" class="form-horizontal" action="/tasks" method="post">
2
-
3
- <div class="control-group">
4
- <label for="task-description" class="control-label">Description</label>
5
- <div class="controls">
6
- <input type="textfield" id="task-description" name="task[description]" value="<%= @task.description unless @task.nil? %>" />
7
- </div>
8
- </div>
9
-
10
- <div class="control-group">
11
- <label for="task-project" class="control-label">Project</label>
12
- <div class="controls">
13
- <input type="textfield" id="task-project" name="task[project]" value="<%= @task.project unless @task.nil? %>" autocomplete="off" />
14
- </div>
15
- </div>
16
-
17
- <div class="control-group">
18
- <label for="task-due" class="control-label">Due Date</label>
19
- <div class="controls">
20
- <input class="date-picker" type="textfield" id="task-due" name="task[due]" value="<%= @task.due unless @task.nil? %>" data-date-format="<%= @date_format %>" />
21
- </div>
22
- </div>
23
-
24
- <div class="control-group">
25
- <label for="task-tags" class="control-label">Tags</label>
26
- <div class="controls">
27
- <input type="textfield" id="task-tags" name="task[tags]" value="<%= @task.tags.join(', ') unless @task.nil? %>" autocomplete="off" />
28
- <span class="help-block">Enter tags separated by commas or spaces (e.g. <em>each, word will,be a tag</em>)</span>
29
- </div>
30
- </div>
31
-
32
- <div class="control-group">
33
- <div class="controls">
34
- <button type="submit" class="btn">Create Task</button>
35
- </div>
36
- </div>
37
-
38
- </form>