taskwarrior-web 1.0.14 → 1.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.
@@ -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>