taskwarrior-web 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  .rvmrc
2
+ coverage
2
3
  pkg
3
4
  coverage
5
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -1,3 +1,13 @@
1
+ ## v0.0.14 (2/9/12)
2
+
3
+ * Merged in major refactoring to allow for easier support of task 2 and
4
+ 1 simultaneously. Note that taskwarrior-web still only supports task 1, but
5
+ adding support for task 2 should now be easier.
6
+ * Removed the ability to mark a task as complete. This was really buggy to
7
+ begin with, and will need to wait until task 2.
8
+ * Fixed project autocomplete. Now it should actually work.
9
+ * Added a new tab for "Waiting" tasks (where status:waiting)
10
+
1
11
  ## v0.0.13 (2/6/12)
2
12
 
3
13
  * Adding Fluid app dock icon. The dock icons should now show a number of
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in taskwarrior-web.gemspec
4
4
  gemspec
5
+
6
+ gem 'rb-fsevent', :require => false
7
+ gem 'growl', :require => false
@@ -0,0 +1,12 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'bundler' do
5
+ watch('Gemfile')
6
+ watch('taskwarrior-web.gemspec')
7
+ end
8
+
9
+ guard 'rspec', :version => 2 do
10
+ watch(/^lib\/(.+)\.rb$/) { "spec" }
11
+ watch(/^spec\/(.+)\.rb$/) { "spec" }
12
+ end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Web Interface for Taskwarrior
1
+ # A Web Interface for Taskwarrior
2
2
 
3
3
  A lightweight, Sinatra-based web interface for the
4
4
  wonderful [Taskwarrior](http://taskwarrior.org/) todo application.
@@ -25,14 +25,18 @@ an executable, so all options for Vegas are valid for `task-web`. Type
25
25
  The current featureset includes:
26
26
 
27
27
  * Viewing tasks (duh) sorted and grouped in various ways.
28
- * Marking a pending task as done.
29
28
  * Creating a new task with a due date, project, and tags.
30
29
  * `task-web` will pull your `task` config (from `.taskrc`) and use it to
31
30
  determine date formatting and when an upcoming task should be marked as
32
31
  "due".
32
+ * If you are on a Mac and use Fluid.app, you get a dock badge showing the
33
+ number of pending tasks.
33
34
 
34
35
  I'm looking to include more features once `task` supports issuing commands via
35
- UUID.
36
+ UUID, like:
37
+
38
+ * Marking a pending task as done.
39
+ * Deleting tasks
36
40
 
37
41
  ## Known Issues
38
42
 
@@ -42,9 +46,6 @@ UUID.
42
46
  Support for 1.8 will happen at some point.
43
47
  * The "View as list"/"View as Grid" links do nothing right now. (They will
44
48
  soon).
45
- * There are occasionally pretty severe race conditions due to the way that
46
- `task` assigns IDs to tasks. This will no longer be the case when UUIDs are
47
- implemented in `task`.
48
49
 
49
50
  ## Marginalia
50
51
 
@@ -2,10 +2,10 @@
2
2
 
3
3
  require 'sinatra'
4
4
  require 'erb'
5
- require 'parseconfig'
6
- require 'json'
7
5
  require 'time'
8
6
  require 'rinku'
7
+ require 'taskwarrior-web/config'
8
+ require 'taskwarrior-web/helpers'
9
9
  require 'digest'
10
10
 
11
11
  module TaskwarriorWeb
@@ -22,66 +22,18 @@ module TaskwarriorWeb
22
22
  def authorized?
23
23
  @auth ||= Rack::Auth::Basic::Request.new(request.env)
24
24
  @auth.provided? && @auth.basic? && @auth.credentials &&
25
- @auth.credentials[0] == TaskwarriorWeb::Config.file.get_value('task-web.user') &&
26
- Digest::MD5.hexdigest(@auth.credentials[1]) == TaskwarriorWeb::Config.file.get_value('task-web.passwd')
25
+ @auth.credentials[0] == TaskwarriorWeb::Config.property('task-web.user') &&
26
+ Digest::MD5.hexdigest(@auth.credentials[1]) == TaskwarriorWeb::Config.property('task-web.passwd')
27
27
  end
28
28
 
29
29
  # Before filter
30
30
  before do
31
31
  @current_page = request.path_info
32
- protected! if TaskwarriorWeb::Config.file.get_value('task-web.user')
32
+ protected! if TaskwarriorWeb::Config.property('task-web.user')
33
33
  end
34
34
 
35
35
  # Helpers
36
- helpers do
37
-
38
- def format_date(timestamp)
39
- format = TaskwarriorWeb::Config.file.get_value('dateformat') || 'm/d/Y'
40
- subbed = format.gsub(/([a-zA-Z])/, '%\1')
41
- Time.parse(timestamp).strftime(subbed)
42
- end
43
-
44
- def colorize_date(timestamp)
45
- return if timestamp.nil?
46
- due_def = TaskwarriorWeb::Config.file.get_value('due').to_i || 5
47
- time = Time.parse(timestamp)
48
- case true
49
- when Time.now.strftime('%D') == time.strftime('%D') then 'today'
50
- when Time.now.to_i > time.to_i then 'overdue'
51
- when (time.to_i - Time.now.to_i) < (due_def * 86400) then 'due'
52
- else 'regular'
53
- end
54
- end
55
-
56
- def linkify(item, method)
57
- return if item.nil?
58
- case method.to_s
59
- when 'project'
60
- item.downcase.gsub('.', '--')
61
- end
62
- end
63
-
64
- def auto_link(text)
65
- Rinku.auto_link(text, :all, 'target="_blank"')
66
- end
67
-
68
- def subnav(type)
69
- case type
70
- when 'tasks' then
71
- { '/tasks/pending' => "Pending (#{TaskwarriorWeb::Task.count(:status => 'pending')})",
72
- '/tasks/completed' => "Completed",
73
- '/tasks/deleted' => 'Deleted'
74
- }
75
- when 'projects'
76
- {
77
- '/projects/overview' => 'Overview'
78
- }
79
- else
80
- { }
81
- end
82
- end
83
-
84
- end
36
+ helpers TaskwarriorWeb::App::Helpers
85
37
 
86
38
  # Redirects
87
39
  get '/' do
@@ -93,7 +45,7 @@ module TaskwarriorWeb
93
45
 
94
46
  # Task routes
95
47
  get '/tasks/:status/?' do
96
- pass unless ['pending', 'completed', 'deleted'].include?(params[:status])
48
+ pass unless ['pending', 'waiting', 'completed', 'deleted'].include?(params[:status])
97
49
  @title = "#{params[:status].capitalize} Tasks"
98
50
  @subnav = subnav('tasks')
99
51
  @tasks = TaskwarriorWeb::Task.find_by_status(params[:status]).sort_by! { |x| [x.priority.nil?.to_s, x.priority.to_s, x.due.nil?.to_s, x.due.to_s, x.project.to_s] }
@@ -103,7 +55,7 @@ module TaskwarriorWeb
103
55
  get '/tasks/new/?' do
104
56
  @title = 'New Task'
105
57
  @subnav = subnav('tasks')
106
- @date_format = TaskwarriorWeb::Config.file.get_value('dateformat') || 'm/d/yy'
58
+ @date_format = TaskwarriorWeb::Config.dateformat || 'm/d/yy'
107
59
  @date_format.gsub!('Y', 'yy')
108
60
  erb :task_form
109
61
  end
@@ -124,11 +76,6 @@ module TaskwarriorWeb
124
76
  end
125
77
  end
126
78
 
127
- post '/tasks/:id/complete' do
128
- TaskwarriorWeb::Task.complete!(params[:id])
129
- redirect '/tasks/pending'
130
- end
131
-
132
79
  # Projects
133
80
  get '/projects' do
134
81
  redirect '/projects/overview'
@@ -144,9 +91,8 @@ module TaskwarriorWeb
144
91
  get '/projects/:name/?' do
145
92
  @subnav = subnav('projects')
146
93
  subbed = params[:name].gsub('--', '.')
147
- @tasks = TaskwarriorWeb::Task.query('status.not' => 'deleted', 'project' => subbed).sort_by! { |x| [x.priority.nil?.to_s, x.priority.to_s, x.due.nil?.to_s, x.due.to_s] }
148
- regex = Regexp.new("^#{subbed}$", Regexp::IGNORECASE)
149
- @title = @tasks.select { |t| t.project.match(regex) }.first.project
94
+ @tasks = TaskwarriorWeb::Task.query('status.not' => 'deleted', :project => subbed).sort_by! { |x| [x.priority.nil?.to_s, x.priority.to_s, x.due.nil?.to_s, x.due.to_s] }
95
+ @title = @tasks.select { |t| t.project.match(/^#{subbed}$/i) }.first.project
150
96
  erb :project
151
97
  end
152
98
 
@@ -157,7 +103,7 @@ module TaskwarriorWeb
157
103
  # AJAX callbacks
158
104
  get '/ajax/projects/?' do
159
105
  projects = TaskwarriorWeb::Task.query('status.not' => 'deleted').collect { |t| t.project }
160
- projects.compact!.uniq!.to_json
106
+ projects.compact.uniq.select {|proj| proj.start_with?(params[:term]) }.to_json
161
107
  end
162
108
 
163
109
  get '/ajax/tags/?' do
@@ -181,8 +127,8 @@ module TaskwarriorWeb
181
127
 
182
128
  def passes_validation(item, method)
183
129
  results = []
184
- case method.to_s
185
- when 'task'
130
+ case method
131
+ when :task
186
132
  if item['description'].empty?
187
133
  results << 'You must provide a description'
188
134
  end
@@ -0,0 +1,25 @@
1
+ require 'taskwarrior-web/runner'
2
+
3
+ module TaskwarriorWeb
4
+ class Command
5
+
6
+ attr_accessor :command, :id, :params
7
+
8
+ def initialize(command, id = nil, *args)
9
+ @command = command if command
10
+ @id = id if id
11
+ @params = args.last.is_a?(::Hash) ? args.pop : {}
12
+ end
13
+
14
+ def run
15
+ if @command
16
+ TaskwarriorWeb::Runner.run(self)
17
+ else
18
+ raise MissingCommandError
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ class MissingCommandError < Exception; end
25
+ end
@@ -1,11 +1,24 @@
1
+ require 'parseconfig'
2
+
1
3
  module TaskwarriorWeb
2
- module Config
4
+ class Config
3
5
 
4
- extend self
6
+ def self.task_version
7
+ # TODO: Parse the actual task version
8
+ 1
9
+ end
5
10
 
6
- def file
11
+ def self.file
7
12
  @file ||= ParseConfig.new("#{Dir.home}/.taskrc")
8
13
  end
9
14
 
15
+ def self.property(prop)
16
+ self.file.get_value(prop)
17
+ end
18
+
19
+ def self.method_missing(method)
20
+ self.file.get_value(method)
21
+ end
22
+
10
23
  end
11
24
  end
@@ -0,0 +1,56 @@
1
+ require 'taskwarrior-web/config'
2
+
3
+ module TaskwarriorWeb
4
+ class App < Sinatra::Base
5
+ module Helpers
6
+
7
+ def format_date(timestamp)
8
+ format = TaskwarriorWeb::Config.dateformat || 'm/d/Y'
9
+ subbed = format.gsub(/([a-zA-Z])/, '%\1')
10
+ Time.parse(timestamp).strftime(subbed)
11
+ end
12
+
13
+ def colorize_date(timestamp)
14
+ return if timestamp.nil?
15
+ due_def = TaskwarriorWeb::Config.due.to_i || 5
16
+ time = Time.parse(timestamp)
17
+ case true
18
+ when Time.now.strftime('%D') == time.strftime('%D') then 'today'
19
+ when Time.now.to_i > time.to_i then 'overdue'
20
+ when (time.to_i - Time.now.to_i) < (due_def * 86400) then 'due'
21
+ else 'regular'
22
+ end
23
+ end
24
+
25
+ def linkify(item, method)
26
+ return if item.nil?
27
+ case method.to_s
28
+ when 'project'
29
+ item.downcase.gsub('.', '--')
30
+ end
31
+ end
32
+
33
+ def auto_link(text)
34
+ Rinku.auto_link(text, :all, 'target="_blank"')
35
+ end
36
+
37
+ def subnav(type)
38
+ case type
39
+ when 'tasks' then
40
+ { '/tasks/pending' => "Pending (#{TaskwarriorWeb::Task.count(:status => :pending)})",
41
+ '/tasks/waiting' => "Waiting",
42
+ '/tasks/completed' => "Completed",
43
+ '/tasks/deleted' => 'Deleted'
44
+ }
45
+ when 'projects'
46
+ {
47
+ '/projects/overview' => 'Overview'
48
+ }
49
+ else
50
+ { }
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -1,12 +1,61 @@
1
+ require 'taskwarrior-web/config'
2
+
1
3
  module TaskwarriorWeb
2
4
  class Runner
3
5
 
4
6
  TASK_BIN = 'task'
5
7
 
6
- def self.run(command)
7
- command = TASK_BIN + " #{command}"
8
- `#{command}`
8
+ TASK_COMMANDS = {
9
+ :add => 'add',
10
+ :query => '_query',
11
+ :count => 'count'
12
+ }
13
+
14
+ def self.run(command_obj)
15
+ command = build(command_obj)
16
+ # Add some logging
17
+ `#{TASK_BIN} #{command}`
18
+ end
19
+
20
+ def self.build(command_obj)
21
+ command = task_command(command_obj)
22
+ command = substitute_parts(command, command_obj) if command =~ /:id/
23
+ params = parsed_params(command_obj.params)
24
+ "#{command}#{params}"
25
+ end
26
+
27
+ def self.task_command(command_obj)
28
+ if TASK_COMMANDS.has_key?(command_obj.command.to_sym)
29
+ TASK_COMMANDS[command_obj.command.to_sym]
30
+ else
31
+ raise InvalidCommandError
32
+ end
33
+ end
34
+
35
+ def self.substitute_parts(task_command, command_obj)
36
+ if command_obj.id
37
+ task_command.gsub(':id', command_obj.id.to_s)
38
+ else
39
+ raise MissingTaskIDError
40
+ end
41
+ end
42
+
43
+ def self.parsed_params(params)
44
+ String.new.tap do |string|
45
+ string << " '#{params.delete(:description)}'" if params.has_key?(:description)
46
+
47
+ if params.has_key?(:tags)
48
+ tags = params.delete(:tags)
49
+ tag_indicator = TaskwarriorWeb::Config.property('tag.indicator') || '+'
50
+ tags.each { |tag| string << " #{tag_indicator}#{tag.to_s}" }
51
+ end
52
+
53
+ params.each { |attr, value| string << " #{attr.to_s}:#{value.to_s}" }
54
+ end
9
55
  end
10
56
 
11
57
  end
58
+
59
+ class InvalidCommandError < Exception; end
60
+ class MissingTaskIDError < Exception; end
12
61
  end
@@ -0,0 +1,11 @@
1
+ module TaskwarriorWeb
2
+ module Runner
3
+ module Version1
4
+
5
+ def self.run
6
+ puts 'Called versioned run method.'
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module TaskwarriorWeb
2
+ module Runner
3
+ module Version2
4
+
5
+ end
6
+ end
7
+ end
@@ -1,4 +1,5 @@
1
- require 'taskwarrior-web/runner'
1
+ require 'json'
2
+ require 'taskwarrior-web/command'
2
3
 
3
4
  module TaskwarriorWeb
4
5
 
@@ -7,7 +8,7 @@ module TaskwarriorWeb
7
8
  #################
8
9
  class Task
9
10
 
10
- attr_accessor :id, :entry, :project, :priority, :uuid, :description, :status,
11
+ attr_accessor :entry, :project, :priority, :uuid, :description, :status,
11
12
  :due, :start, :end, :tags, :depends, :wait, :annotations
12
13
  alias :annotate= :annotations=
13
14
 
@@ -22,19 +23,16 @@ module TaskwarriorWeb
22
23
  end
23
24
 
24
25
  def save!
25
- exclude = ['@description', '@tags']
26
- command = 'add'
27
- command << " '#{description}'"
28
- instance_variables.each do |ivar|
29
- subbed = ivar.to_s.gsub('@', '')
30
- command << " #{subbed}:#{send(subbed.to_sym)}" unless exclude.include?(ivar.to_s)
31
- end
32
- unless tags.nil?
33
- tags.gsub(', ', ',').split(',').each do |tag|
34
- command << " +#{tag}"
35
- end
36
- end
37
- TaskwarriorWeb::Runner.run(command)
26
+ Command.new(:add, nil, self.to_hash).run
27
+ end
28
+
29
+ # Make sure that the tags are an array.
30
+ def tags=(value)
31
+ @tags = value.is_a?(String) ? value.gsub(', ', ',').split(',') : value
32
+ end
33
+
34
+ def to_hash
35
+ Hash[instance_variables.map { |var| [var[1..-1].to_sym, instance_variable_get(var)] }]
38
36
  end
39
37
 
40
38
  ##################################
@@ -44,34 +42,27 @@ module TaskwarriorWeb
44
42
  # Run queries on tasks.
45
43
  def self.query(*args)
46
44
  tasks = []
47
- count = 1
48
-
49
- command = '_query'
50
- args.each do |param|
51
- param.each do |attr, value|
52
- command << " #{attr.to_s}:#{value}"
53
- end
54
- end
55
45
 
56
46
  # Process the JSON data.
57
- json = TaskwarriorWeb::Runner.run(command)
47
+ json = Command.new(:query, nil, *args).run
58
48
  json.strip!
59
49
  json = '[' + json + ']'
60
- results = json == '[No matches.]' ? [] : JSON.parse(json)
50
+ results = json == '[No matches.]' ? [] : ::JSON.parse(json)
61
51
 
62
- results.each do |result|
63
- result[:id] = count
64
- tasks << Task.new(result)
65
- count = count + 1
66
- end
67
- return tasks
52
+ results.each { |result| tasks << Task.new(result) }
53
+ tasks
54
+ end
55
+
56
+ # Get the number of tasks for some paramters
57
+ def self.count(*args)
58
+ Command.new(:count, nil, *args).run.to_s.strip!
68
59
  end
69
60
 
70
61
  # Define method_missing to implement dynamic finder methods
71
62
  def self.method_missing(method_sym, *arguments, &block)
72
63
  match = TaskDynamicFinderMatch.new(method_sym)
73
64
  if match.match?
74
- self.query(match.attribute => arguments.first)
65
+ self.query(match.attribute.to_s => arguments.first.to_s)
75
66
  else
76
67
  super
77
68
  end
@@ -86,28 +77,6 @@ module TaskwarriorWeb
86
77
  end
87
78
  end
88
79
 
89
- # Get the number of tasks for some paramters
90
- def self.count(*args)
91
- command = 'count'
92
- args.each do |param|
93
- param.each do |attr, value|
94
- command << " #{attr.to_s}:#{value}"
95
- end
96
- end
97
- return TaskwarriorWeb::Runner.run(command).strip!
98
- end
99
-
100
- ###############################################
101
- # CLASS METHODS FOR INTERACTING WITH TASKS
102
- # (THESE WILL PROBABLY BECOME INSTANCE METHODS)
103
- ###############################################
104
-
105
- # Mark a task as complete
106
- # TODO: Make into instance method when `task` supports finding by UUID.
107
- def self.complete!(task_id)
108
- TaskwarriorWeb::Runner.run("#{task_id} done")
109
- end
110
-
111
80
  end
112
81
 
113
82
  ###########################################
@@ -1,15 +1,18 @@
1
1
  $(document).ready(function() {
2
2
  initPolling();
3
3
  initTooltips();
4
- initCompleteTask();
5
4
  initDatePicker();
6
5
  initAutocomplete();
7
- refreshDockBadge();
6
+
7
+ // Fluid-specific stuff.
8
+ if (window.fluid) {
9
+ refreshDockBadge();
10
+ }
8
11
  });
9
12
 
10
13
  var initPolling = function() {
11
- var polling;
12
- if (polling = $.cookie('taskwarrior-web-polling')) {
14
+ var polling = $.cookie('taskwarrior-web-polling');
15
+ if (polling) {
13
16
  var pollingInterval = startPolling();
14
17
  } else {
15
18
  $('#polling-info a').text('Start polling');
@@ -54,56 +57,6 @@ var initTooltips = function() {
54
57
  });
55
58
  };
56
59
 
57
- var initCompleteTask = function() {
58
- $('input.pending').live('change', function() {
59
- var checkbox = $(this);
60
- var row = checkbox.closest('tr');
61
- var task_id = $(this).data('task');
62
- $.ajax({
63
- url: '/tasks/' + task_id + '/complete',
64
- type: 'post',
65
- beforeSend: function() {
66
- checkbox.replaceWith('<img src="/images/ajax-loader.gif" />');
67
- },
68
- success: function(data) {
69
- row.fadeOut('slow', function() {
70
- row.remove();
71
- refreshSubnavCount();
72
- refreshDockBadge();
73
- });
74
- }
75
- });
76
- });
77
- };
78
-
79
- var initInPlaceEditing = function() {
80
- // Hide it initially.
81
- $('.inplace-edit').hide();
82
- $('#listing table td').live('mouseover mouseout', function(e) {
83
- if (e.type == 'mouseover') {
84
- $('.inplace-edit', this).show();
85
- } else {
86
- $('.inplace-edit', this).hide();
87
- }
88
- });
89
-
90
- $('.inplace-edit').live('click', function() {
91
- var field = $($(this).siblings('span')[0]);
92
- var formElement = '<input type="text" class="inplace-text" value="'+field.text()+'" />';
93
- formElement += '<button type="submit" class="inplace-submit">Update</button>';
94
- formElement += '<a href="javascript:void(0);" class="inplace-cancel">Cancel</a>';
95
- field.replaceWith(formElement);
96
- $(this).remove();
97
- });
98
-
99
- $('.inplace-cancel').live('click', function() {
100
- var td = $(this).closest('td');
101
- var oldField = '<span class="description">'+$($(this).siblings('.inplace-text')[0]).val()+'</span>';
102
- oldField += '<a class="inplace-edit" href="javascript:void(0);">Edit</a>';
103
- td.html(oldField);
104
- });
105
- };
106
-
107
60
  var initDatePicker = function() {
108
61
  $('.datefield input').datepicker({
109
62
  dateFormat: $('.datefield input').data('format'),
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'taskwarrior-web/app'
3
+
4
+ set :environment, :test
5
+
6
+ describe "My App" do
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ TaskwarriorWeb::App
11
+ end
12
+
13
+ before do
14
+ TaskwarriorWeb::Config.should_receive(:property).with('task-web.user').any_number_of_times.and_return(nil)
15
+ TaskwarriorWeb::Runner.should_receive(:run).any_number_of_times.and_return('{}')
16
+ end
17
+
18
+ describe 'GET /' do
19
+ it 'should redirect to /tasks/pending' do
20
+ get "/"
21
+ follow_redirect!
22
+
23
+ last_request.url.should =~ /tasks\/pending/
24
+ last_response.should be_ok
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'taskwarrior-web/helpers'
3
+
4
+ class TestHelpers
5
+ include TaskwarriorWeb::App::Helpers
6
+ end
7
+
8
+ describe TaskwarriorWeb::App::Helpers do
9
+ let(:helpers) { TestHelpers.new }
10
+
11
+ describe '#format_date' do
12
+ context 'with no format specified' do
13
+ before do
14
+ TaskwarriorWeb::Config.should_receive(:dateformat).any_number_of_times.and_return(nil)
15
+ end
16
+
17
+ it 'should format various dates and times to the default format' do
18
+ helpers.format_date('2012-01-11 12:23:00').should == '01/11/2012'
19
+ helpers.format_date('2012-01-11').should == '01/11/2012'
20
+ end
21
+ end
22
+
23
+ context 'with a specified date format' do
24
+ before do
25
+ TaskwarriorWeb::Config.should_receive(:dateformat).any_number_of_times.and_return('d/m/Y')
26
+ end
27
+
28
+ it 'should format dates using the specified format' do
29
+ helpers.format_date('2012-01-11 12:23:00').should == '11/01/2012'
30
+ helpers.format_date('2012-01-11').should == '11/01/2012'
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'taskwarrior-web/command'
3
+
4
+ describe TaskwarriorWeb::Command do
5
+ describe '.initialize' do
6
+ context 'when the command, id, and params are specified' do
7
+ it 'should set the passed variables' do
8
+ command = TaskwarriorWeb::Command.new('test', 3, :hello => :hi, :none => :none)
9
+ command.command.should eq('test')
10
+ command.id.should eq(3)
11
+ command.params.should eq({ :hello => :hi, :none => :none })
12
+ end
13
+ end
14
+
15
+ it 'should not set an @id if none is passed' do
16
+ command = TaskwarriorWeb::Command.new('test', nil, :hello => :hi, :none => :none)
17
+ command.command.should eq('test')
18
+ command.id.should be_nil
19
+ command.params.should eq({ :hello => :hi, :none => :none })
20
+ end
21
+ end
22
+
23
+ describe '#run' do
24
+ before do
25
+ @command = TaskwarriorWeb::Command.new('test', 4)
26
+ end
27
+
28
+ it 'should pass the object to the Runner' do
29
+ TaskwarriorWeb::Runner.should_receive(:run).with(@command).and_return('{}')
30
+ @command.run
31
+ end
32
+
33
+ it 'should raise an exception if no command is specified' do
34
+ command = TaskwarriorWeb::Command.new(nil, 5)
35
+ expect { command.run }.to raise_error(TaskwarriorWeb::MissingCommandError)
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,74 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'taskwarrior-web/runner'
3
+
4
+ describe TaskwarriorWeb::Runner do
5
+ before do
6
+ @command = TaskwarriorWeb::Command.new(:add)
7
+ end
8
+
9
+ describe '.run' do
10
+ context 'valid, without a command' do
11
+ before do
12
+ TaskwarriorWeb::Runner.should_receive(:`).and_return('{}')
13
+ end
14
+
15
+ it 'should return the stdout' do
16
+ TaskwarriorWeb::Runner.run(@command).should eq('{}')
17
+ end
18
+
19
+ it 'should not call .substitute_parts if not necessary' do
20
+ TaskwarriorWeb::Runner.should_not_receive(:substitute_parts)
21
+ TaskwarriorWeb::Runner.run(@command)
22
+ end
23
+ end
24
+
25
+ context 'invalid' do
26
+ it 'should throw an exception if the command is not valid' do
27
+ @command.command = :test
28
+ expect { TaskwarriorWeb::Runner.run(@command) }.to raise_error(TaskwarriorWeb::InvalidCommandError)
29
+ end
30
+ end
31
+
32
+ context 'with a given command' do
33
+ it 'should execute the given command' do
34
+ TaskwarriorWeb::Runner.should_receive(:`).with('task add').and_return('{}')
35
+ TaskwarriorWeb::Runner.run(@command)
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '.substitute_parts' do
41
+ it 'should replace the :id string with the given task ID' do
42
+ command = TaskwarriorWeb::Command.new(:complete, 4)
43
+ TaskwarriorWeb::Runner.substitute_parts(':id done', command).should eq('4 done')
44
+ end
45
+
46
+ it 'should throw an error if the command has no task ID' do
47
+ expect { TaskwarriorWeb::Runner.substitute_parts(':id done', @command) }.to raise_error(TaskwarriorWeb::MissingTaskIDError)
48
+ end
49
+ end
50
+
51
+ describe '.parsed_params' do
52
+ it 'should create a string from the passed paramters' do
53
+ command = TaskwarriorWeb::Command.new(:query, nil, :test => 14, :none => :none, :hello => :hi)
54
+ TaskwarriorWeb::Runner.parsed_params(command.params).should eq(' test:14 none:none hello:hi')
55
+ end
56
+
57
+ it 'should prefix tags with the tag.indicator if specified' do
58
+ TaskwarriorWeb::Config.should_receive(:property).with('tag.indicator').and_return(';')
59
+ command = TaskwarriorWeb::Command.new(:add, nil, :tags => [:today, :tomorrow])
60
+ TaskwarriorWeb::Runner.parsed_params(command.params).should eq(' ;today ;tomorrow')
61
+ end
62
+
63
+ it 'should prefix tags with a + if no tag.indicator is specified' do
64
+ TaskwarriorWeb::Config.should_receive(:property).with('tag.indicator').and_return(nil)
65
+ command = TaskwarriorWeb::Command.new(:add, nil, :tags => [:today, :tomorrow])
66
+ TaskwarriorWeb::Runner.parsed_params(command.params).should eq(' +today +tomorrow')
67
+ end
68
+
69
+ it 'should pull out the description parameter' do
70
+ command = TaskwarriorWeb::Command.new(:add, nil, :description => 'Hello', :status => :pending)
71
+ TaskwarriorWeb::Runner.parsed_params(command.params).should eq(" 'Hello' status:pending")
72
+ end
73
+ end
74
+ end
@@ -1,6 +1,9 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
1
2
  require 'taskwarrior-web/task'
2
3
 
3
4
  describe TaskwarriorWeb::Task do
5
+ RSpec::Mocks::setup(TaskwarriorWeb::Runner)
6
+ TaskwarriorWeb::Runner.stub(:run) { '{}' }
4
7
 
5
8
  describe '#initialize' do
6
9
  it 'should assign the passed attributes' do
@@ -14,4 +17,82 @@ describe TaskwarriorWeb::Task do
14
17
  task.respond_to?(:bogus).should be_false
15
18
  end
16
19
  end
20
+
21
+ describe '.query' do
22
+ it 'should create and run a new Command object' do
23
+ command = TaskwarriorWeb::Command.new(:query)
24
+ TaskwarriorWeb::Command.should_receive(:new).with(:query, nil).and_return(command)
25
+ command.should_receive(:run).and_return('{}')
26
+ TaskwarriorWeb::Task.query
27
+ end
28
+
29
+ it 'should parse the JSON received from the `task` command' do
30
+ TaskwarriorWeb::Runner.should_receive(:run).and_return('{}')
31
+ ::JSON.should_receive(:parse).with('[{}]').and_return([])
32
+ TaskwarriorWeb::Task.query
33
+ end
34
+
35
+ it 'should not parse the results when there are no matching tasks' do
36
+ TaskwarriorWeb::Runner.should_receive(:run).and_return('No matches.')
37
+ ::JSON.should_not_receive(:parse)
38
+ TaskwarriorWeb::Task.query
39
+ end
40
+ end
41
+
42
+ describe '.count' do
43
+ it 'create and run an new command object' do
44
+ command = TaskwarriorWeb::Command.new(:count)
45
+ TaskwarriorWeb::Command.should_receive(:new).with(:count, nil).and_return(command)
46
+ command.should_receive(:run).and_return('{}')
47
+ TaskwarriorWeb::Task.count
48
+ end
49
+ end
50
+
51
+ describe '#tags=' do
52
+ it 'should convert a string to an array when initializing' do
53
+ task = TaskwarriorWeb::Task.new(:tags => 'hi there, twice')
54
+ task.tags.should eq(['hi there', 'twice'])
55
+ end
56
+
57
+ it 'should convert a string to an array when setting explicitly' do
58
+ task = TaskwarriorWeb::Task.new
59
+ task.tags = 'hello, twice,thrice'
60
+ task.tags.should eq(['hello', 'twice', 'thrice'])
61
+ end
62
+ end
63
+
64
+ describe '#to_hash' do
65
+ before do
66
+ @task = TaskwarriorWeb::Task.new(:description => 'Testing', :due => '12/2/12', :tags => 'hello, twice')
67
+ end
68
+
69
+ it 'should return a hash' do
70
+ @task.to_hash.should be_a(Hash)
71
+ end
72
+
73
+ it 'should have keys for each of the object\'s instance variables' do
74
+ @task.to_hash.should eq({:description => 'Testing', :due => '12/2/12', :tags => ['hello', 'twice']})
75
+ end
76
+ end
77
+
78
+ describe '.method_missing' do
79
+ it 'should call the query method for find_by queries' do
80
+ TaskwarriorWeb::Task.should_receive(:query).with('status' => 'pending')
81
+ TaskwarriorWeb::Task.find_by_status(:pending)
82
+ end
83
+
84
+ it 'should call pass other methods to super if not find_by_*' do
85
+ TaskwarriorWeb::Task.should_not_receive(:query)
86
+ Object.should_receive(:method_missing).with(:do_a_thing, anything)
87
+ TaskwarriorWeb::Task.do_a_thing(:pending)
88
+ end
89
+
90
+ it 'should make the class respond to find_by queries' do
91
+ TaskwarriorWeb::Task.should respond_to(:find_by_test)
92
+ end
93
+
94
+ it 'should not add support for obviously bogus methods' do
95
+ TaskwarriorWeb::Task.should_not respond_to(:wefiohhohiihihih)
96
+ end
97
+ end
17
98
  end
@@ -0,0 +1,18 @@
1
+ require 'sinatra'
2
+ require 'rack/test'
3
+ require 'rspec'
4
+
5
+ # Simplecov
6
+ require 'simplecov'
7
+ SimpleCov.start do
8
+ add_filter "spec"
9
+ end
10
+
11
+ # Requires supporting files with custom matchers and macros, etc,
12
+ # in ./support/ and its subdirectories.
13
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
14
+
15
+ RSpec.configure do |config|
16
+ config.mock_with :rspec
17
+ config.include(RSpec::Mocks::Methods)
18
+ end
@@ -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 = '0.0.13'
6
+ s.version = '0.0.14'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Jake Bell"]
9
9
  s.email = ["jake@theunraveler.com"]
@@ -21,7 +21,11 @@ Gem::Specification.new do |s|
21
21
  s.add_dependency('rinku')
22
22
 
23
23
  s.add_development_dependency('rake')
24
+ s.add_development_dependency('rack-test')
24
25
  s.add_development_dependency('rspec')
26
+ s.add_development_dependency('simplecov')
27
+ s.add_development_dependency('guard-rspec')
28
+ s.add_development_dependency('guard-bundler')
25
29
 
26
30
  s.files = `git ls-files`.split("\n")
27
31
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -2,7 +2,6 @@
2
2
  <table>
3
3
  <thead>
4
4
  <tr>
5
- <th></th>
6
5
  <th>Description</th>
7
6
  <th>Project</th>
8
7
  <th>Due</th>
@@ -14,11 +13,9 @@
14
13
  <% @tasks.each do |task| %>
15
14
  <% if task.status == 'pending' %>
16
15
  <tr class="<%= colorize_date(task.due) %>">
17
- <td><input type="checkbox" <%= 'checked="checked" ' if task.status == 'completed' %> class="<%= task.status %>" data-task="<%= task.id %>" /></td>
18
- <% else %>
19
- <tr class="<%= task.status %>">
20
- <td></td>
21
- <% end %>
16
+ <% else %>
17
+ <tr class="<%= task.status %>">
18
+ <% end %>
22
19
  <td>
23
20
  <%= task.description %>
24
21
  <% unless task.annotations.nil? || task.annotations.empty? %>
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: 0.0.13
4
+ version: 0.0.14
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-02-07 00:00:00.000000000 Z
12
+ date: 2012-02-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
16
- requirement: &2162191880 !ruby/object:Gem::Requirement
16
+ requirement: &2152168480 !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: *2162191880
24
+ version_requirements: *2152168480
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: parseconfig
27
- requirement: &2162191420 !ruby/object:Gem::Requirement
27
+ requirement: &2152167760 !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: *2162191420
35
+ version_requirements: *2152167760
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: vegas
38
- requirement: &2162191000 !ruby/object:Gem::Requirement
38
+ requirement: &2152167020 !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: *2162191000
46
+ version_requirements: *2152167020
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rinku
49
- requirement: &2162190580 !ruby/object:Gem::Requirement
49
+ requirement: &2152166160 !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: *2162190580
57
+ version_requirements: *2152166160
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &2162190160 !ruby/object:Gem::Requirement
60
+ requirement: &2152165600 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,54 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *2162190160
68
+ version_requirements: *2152165600
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-test
71
+ requirement: &2152165000 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2152165000
69
80
  - !ruby/object:Gem::Dependency
70
81
  name: rspec
71
- requirement: &2162189740 !ruby/object:Gem::Requirement
82
+ requirement: &2152164380 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *2152164380
91
+ - !ruby/object:Gem::Dependency
92
+ name: simplecov
93
+ requirement: &2152023060 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *2152023060
102
+ - !ruby/object:Gem::Dependency
103
+ name: guard-rspec
104
+ requirement: &2152021400 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *2152021400
113
+ - !ruby/object:Gem::Dependency
114
+ name: guard-bundler
115
+ requirement: &2152020560 !ruby/object:Gem::Requirement
72
116
  none: false
73
117
  requirements:
74
118
  - - ! '>='
@@ -76,7 +120,7 @@ dependencies:
76
120
  version: '0'
77
121
  type: :development
78
122
  prerelease: false
79
- version_requirements: *2162189740
123
+ version_requirements: *2152020560
80
124
  description: This gem provides a graphical frontend for the Taskwarrior task manager.
81
125
  It is based on Sinatra.
82
126
  email:
@@ -87,18 +131,23 @@ extensions: []
87
131
  extra_rdoc_files: []
88
132
  files:
89
133
  - .gitignore
134
+ - .rspec
90
135
  - .travis.yml
91
136
  - CHANGELOG.md
92
137
  - Gemfile
93
- - Gemfile.lock
138
+ - Guardfile
94
139
  - README.md
95
140
  - Rakefile
96
141
  - bin/task-web
97
142
  - config.ru
98
143
  - lib/taskwarrior-web.rb
99
144
  - lib/taskwarrior-web/app.rb
145
+ - lib/taskwarrior-web/command.rb
100
146
  - lib/taskwarrior-web/config.rb
147
+ - lib/taskwarrior-web/helpers.rb
101
148
  - lib/taskwarrior-web/runner.rb
149
+ - lib/taskwarrior-web/runners/v1.rb
150
+ - lib/taskwarrior-web/runners/v2.rb
102
151
  - lib/taskwarrior-web/task.rb
103
152
  - public/css/gh-buttons.css
104
153
  - public/css/jquery-ui.css
@@ -128,7 +177,12 @@ files:
128
177
  - public/js/jquery.min.js
129
178
  - public/js/jquery.tagsinput.js
130
179
  - public/js/jquery.tipsy.js
180
+ - spec/app/app_spec.rb
181
+ - spec/app/helpers_spec.rb
182
+ - spec/models/command_spec.rb
183
+ - spec/models/runner_spec.rb
131
184
  - spec/models/task_spec.rb
185
+ - spec/spec_helper.rb
132
186
  - taskwarrior-web.gemspec
133
187
  - views/404.erb
134
188
  - views/_navigation.erb
@@ -157,7 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
211
  version: '0'
158
212
  segments:
159
213
  - 0
160
- hash: -705373290022093367
214
+ hash: -1565305535582289311
161
215
  requirements: []
162
216
  rubyforge_project: taskwarrior-web
163
217
  rubygems_version: 1.8.11
@@ -165,4 +219,9 @@ signing_key:
165
219
  specification_version: 3
166
220
  summary: Web frontend for taskwarrior command line task manager.
167
221
  test_files:
222
+ - spec/app/app_spec.rb
223
+ - spec/app/helpers_spec.rb
224
+ - spec/models/command_spec.rb
225
+ - spec/models/runner_spec.rb
168
226
  - spec/models/task_spec.rb
227
+ - spec/spec_helper.rb
@@ -1,42 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- taskwarrior-web (0.0.13)
5
- parseconfig
6
- rinku
7
- sinatra
8
- vegas
9
-
10
- GEM
11
- remote: http://rubygems.org/
12
- specs:
13
- diff-lcs (1.1.3)
14
- parseconfig (0.5.2)
15
- rack (1.4.1)
16
- rack-protection (1.2.0)
17
- rack
18
- rake (0.9.2.2)
19
- rinku (1.5.0)
20
- rspec (2.8.0)
21
- rspec-core (~> 2.8.0)
22
- rspec-expectations (~> 2.8.0)
23
- rspec-mocks (~> 2.8.0)
24
- rspec-core (2.8.0)
25
- rspec-expectations (2.8.0)
26
- diff-lcs (~> 1.1.2)
27
- rspec-mocks (2.8.0)
28
- sinatra (1.3.2)
29
- rack (~> 1.3, >= 1.3.6)
30
- rack-protection (~> 1.2)
31
- tilt (~> 1.3, >= 1.3.3)
32
- tilt (1.3.3)
33
- vegas (0.1.11)
34
- rack (>= 1.0.0)
35
-
36
- PLATFORMS
37
- ruby
38
-
39
- DEPENDENCIES
40
- rake
41
- rspec
42
- taskwarrior-web!