taskwarrior-web 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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!