taskwarrior-web 0.0.1

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.
Files changed (49) hide show
  1. data/.gitignore +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +4 -0
  4. data/README.md +6 -0
  5. data/Rakefile +2 -0
  6. data/bin/task-web +10 -0
  7. data/config.ru +9 -0
  8. data/lib/taskwarrior-web/app.rb +172 -0
  9. data/lib/taskwarrior-web/config.rb +11 -0
  10. data/lib/taskwarrior-web/task.rb +131 -0
  11. data/lib/taskwarrior-web/version.rb +3 -0
  12. data/lib/taskwarrior-web.rb +8 -0
  13. data/public/css/gh-buttons.css +388 -0
  14. data/public/css/jquery-ui.css +738 -0
  15. data/public/css/jquery.tagsinput.css +6 -0
  16. data/public/css/styles.css +283 -0
  17. data/public/css/tipsy.css +7 -0
  18. data/public/favicon.ico +0 -0
  19. data/public/images/ajax-loader.gif +0 -0
  20. data/public/images/arrow_right_black.png +0 -0
  21. data/public/images/arrow_right_grey.png +0 -0
  22. data/public/images/bg_fallback.png +0 -0
  23. data/public/images/gh-icons.png +0 -0
  24. data/public/images/grid-view.png +0 -0
  25. data/public/images/icon_sprite.png +0 -0
  26. data/public/images/list-view.png +0 -0
  27. data/public/images/logo.png +0 -0
  28. data/public/images/progress_bar.gif +0 -0
  29. data/public/images/slider_handles.png +0 -0
  30. data/public/images/subnav_background.gif +0 -0
  31. data/public/images/tab_background.gif +0 -0
  32. data/public/images/tipsy.gif +0 -0
  33. data/public/images/ui-icons_222222_256x240.png +0 -0
  34. data/public/images/ui-icons_454545_256x240.png +0 -0
  35. data/public/js/application.js +130 -0
  36. data/public/js/jquery-ui.min.js +163 -0
  37. data/public/js/jquery.cookie.js +96 -0
  38. data/public/js/jquery.min.js +16 -0
  39. data/public/js/jquery.tagsinput.js +218 -0
  40. data/public/js/jquery.tipsy.js +104 -0
  41. data/taskwarrior-web.gemspec +25 -0
  42. data/views/404.erb +3 -0
  43. data/views/_navigation.erb +21 -0
  44. data/views/layout.erb +47 -0
  45. data/views/listing.erb +29 -0
  46. data/views/project.erb +25 -0
  47. data/views/projects.erb +35 -0
  48. data/views/task_form.erb +28 -0
  49. metadata +134 -0
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ruby-1.9.2@taskwarrior-web
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in taskwarrior-web.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # Web Interface for Taskwarrior
2
+
3
+ This will eventually be a lightweight, Sinatra-based web interface for the
4
+ wonderful (Taskwarrior)[http://taskwarrior.org/] todo application.
5
+
6
+ It is still very much a work in progress.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/task-web ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-2-27.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/taskwarrior-web")
7
+ require 'vegas'
8
+
9
+ Vegas::Runner.new(TaskwarriorWeb::App, 'taskwarrior-web')
10
+
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # To use with thin
2
+ # thin start -p PORT -R config.ru
3
+ require File.join(File.dirname(__FILE__), 'lib', 'taskwarrior-web')
4
+
5
+ disable :run
6
+ TaskwarriorWeb::App.set({
7
+ :environment => :production
8
+ })
9
+ run TaskwarriorWeb::App
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra'
4
+ require 'erb'
5
+ require 'parseconfig'
6
+ require 'json'
7
+ require 'time'
8
+
9
+ module TaskwarriorWeb
10
+ class App < Sinatra::Base
11
+
12
+ @@root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
13
+ set :root, @@root
14
+ set :app_file, __FILE__
15
+
16
+ # Before filter
17
+ before do
18
+ @current_page = request.path_info
19
+ end
20
+
21
+ # Helpers
22
+ helpers do
23
+
24
+ def format_date(timestamp)
25
+ format = TaskwarriorWeb::Config.file.get_value('dateformat') || 'm/d/Y'
26
+ subbed = format.gsub(/([a-zA-Z])/, '%\1')
27
+ Time.parse(timestamp).strftime(subbed)
28
+ end
29
+
30
+ def colorize_date(timestamp)
31
+ return if timestamp.nil?
32
+ due_def = TaskwarriorWeb::Config.file.get_value('due').to_i || 5
33
+ time = Time.parse(timestamp)
34
+ case true
35
+ when Time.now.strftime('%D') == time.strftime('%D') then 'today'
36
+ when Time.now.to_i > time.to_i then 'overdue'
37
+ when (time.to_i - Time.now.to_i) < (due_def * 86400) then 'due'
38
+ else 'regular'
39
+ end
40
+ end
41
+
42
+ def linkify(item, method)
43
+ return if item.nil?
44
+ case method.to_s
45
+ when 'project'
46
+ item.downcase.gsub('.', '--')
47
+ end
48
+ end
49
+
50
+ def subnav(type)
51
+ case type
52
+ when 'tasks' then
53
+ { '/tasks/pending' => "Pending (#{TaskwarriorWeb::Task.count(:status => 'pending')})",
54
+ '/tasks/completed' => "Completed",
55
+ '/tasks/deleted' => 'Deleted'
56
+ }
57
+ when 'projects'
58
+ {
59
+ '/projects/overview' => 'Overview'
60
+ }
61
+ else
62
+ { }
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ # Redirects
69
+ get '/' do
70
+ redirect '/tasks/pending'
71
+ end
72
+ get '/tasks/?' do
73
+ redirect '/tasks/pending'
74
+ end
75
+
76
+ # Task routes
77
+ get '/tasks/:status/?' do
78
+ pass unless ['pending', 'completed', 'deleted'].include?(params[:status])
79
+ @title = "#{params[:status].capitalize} Tasks"
80
+ @subnav = subnav('tasks')
81
+ @tasks = TaskwarriorWeb::Task.find_by_status(params[:status]).sort_by! { |x| [x.due.nil?.to_s, x.due.to_s, x.project.to_s] }
82
+ erb :listing
83
+ end
84
+
85
+ get '/tasks/new/?' do
86
+ @title = 'New Task'
87
+ @subnav = subnav('tasks')
88
+ @date_format = TaskwarriorWeb::Config.file.get_value('dateformat') || 'm/d/yy'
89
+ @date_format.gsub!('Y', 'yy')
90
+ erb :task_form
91
+ end
92
+
93
+ post '/tasks/new/?' do
94
+ results = passes_validation(params[:task], :task)
95
+ if results.empty?
96
+ task = TaskwarriorWeb::Task.new(params[:task])
97
+ task.save!.to_s
98
+ redirect '/tasks'
99
+ else
100
+ @task = params[:task]
101
+ @messages = []
102
+ results.each do |result|
103
+ @messages << { :severity => 'error', :message => result }
104
+ end
105
+ redirect '/tasks/new'
106
+ end
107
+ end
108
+
109
+ post '/tasks/:id/complete' do
110
+ TaskwarriorWeb::Task.complete!(params[:id])
111
+ redirect '/tasks/pending'
112
+ end
113
+
114
+ # Projects
115
+ get '/projects' do
116
+ redirect '/projects/overview'
117
+ end
118
+
119
+ get '/projects/overview/?' do
120
+ @title = 'Projects'
121
+ @subnav = subnav('projects')
122
+ @tasks = TaskwarriorWeb::Task.query('status.not' => 'deleted', 'project.not' => '').group_by { |x| x.project.to_s }
123
+ erb :projects
124
+ end
125
+
126
+ get '/projects/:name/?' do
127
+ @subnav = subnav('projects')
128
+ subbed = params[:name].gsub('--', '.')
129
+ @tasks = TaskwarriorWeb::Task.query('status.not' => 'deleted', 'project' => subbed).sort_by! { |x| [x.due.nil?.to_s, x.due.to_s] }
130
+ regex = Regexp.new("^#{subbed}$", Regexp::IGNORECASE)
131
+ @title = @tasks.select { |t| t.project.match(regex) }.first.project
132
+ erb :project
133
+ end
134
+
135
+ # Reporting
136
+ get '/reports' do
137
+ end
138
+
139
+ # AJAX callbacks
140
+ get '/ajax/projects/?' do
141
+ projects = TaskwarriorWeb::Task.query('status.not' => 'deleted').collect { |t| t.project }
142
+ projects.compact!.uniq!.to_json
143
+ end
144
+
145
+ get '/ajax/tags/?' do
146
+ tags = []
147
+ TaskwarriorWeb::Task.query('status.not' => 'deleted').each do |task|
148
+ tags = tags + task.tags
149
+ end
150
+ tags.compact!.uniq!.to_json
151
+ end
152
+
153
+ # Error handling
154
+ not_found do
155
+ @title = 'Page Not Found'
156
+ @referrer = request.referrer
157
+ erb :'404'
158
+ end
159
+
160
+ def passes_validation(item, method)
161
+ results = []
162
+ case method.to_s
163
+ when 'task'
164
+ if item['description'].empty?
165
+ results << 'You must provide a description'
166
+ end
167
+ end
168
+ results
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,11 @@
1
+ module TaskwarriorWeb
2
+ module Config
3
+
4
+ extend self
5
+
6
+ def file
7
+ @file ||= ParseConfig.new("#{Dir.home}/.taskrc")
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,131 @@
1
+ module TaskwarriorWeb
2
+
3
+ #################
4
+ # MAIN TASK CLASS
5
+ #################
6
+ class Task
7
+
8
+ TASK_BIN = 'task'
9
+
10
+ attr_accessor :id, :entry, :project, :uuid, :description, :status, :due, :start, :end, :tags
11
+
12
+ ####################################
13
+ # MODEL METHODS FOR INDIVIDUAL TASKS
14
+ ####################################
15
+
16
+ def initialize(attributes = {})
17
+ attributes.each do |attr, value|
18
+ send("#{attr}=", value)
19
+ end
20
+ end
21
+
22
+ def save!
23
+ exclude = ['@description', '@tags']
24
+ command = TASK_BIN + ' add'
25
+ command << " '#{description}'"
26
+ instance_variables.each do |ivar|
27
+ subbed = ivar.to_s.gsub('@', '')
28
+ command << " #{subbed}:#{send(subbed.to_sym)}" unless exclude.include?(ivar.to_s)
29
+ end
30
+ unless tags.nil?
31
+ tags.gsub(', ', ',').split(',').each do |tag|
32
+ command << " +#{tag}"
33
+ end
34
+ end
35
+ puts command
36
+ `#{command}`
37
+ end
38
+
39
+ ##################################
40
+ # CLASS METHODS FOR QUERYING TASKS
41
+ ##################################
42
+
43
+ # Run queries on tasks.
44
+ def self.query(*args)
45
+ tasks = []
46
+ count = 1
47
+
48
+ stdout = TASK_BIN + ' _query'
49
+ args.each do |param|
50
+ param.each do |attr, value|
51
+ stdout << " #{attr.to_s}:#{value}"
52
+ end
53
+ end
54
+
55
+ # Process the JSON data.
56
+ json = `#{stdout}`
57
+ json.strip!
58
+ json = '[' + json + ']'
59
+ results = JSON.parse(json)
60
+
61
+ results.each do |result|
62
+ result[:id] = count
63
+ tasks << Task.new(result)
64
+ count = count + 1
65
+ end
66
+ return tasks
67
+ end
68
+
69
+ # Define method_missing to implement dynamic finder methods
70
+ def self.method_missing(method_sym, *arguments, &block)
71
+ match = TaskDynamicFinderMatch.new(method_sym)
72
+ if match.match?
73
+ self.query(match.attribute => arguments.first)
74
+ else
75
+ super
76
+ end
77
+ end
78
+
79
+ # Implement respond_to? so that our dynamic finders are declared
80
+ def self.respond_to?(method_sym, include_private = false)
81
+ if TaskDynamicFinderMatch.new(method_sym).match?
82
+ true
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ # Get the number of tasks for some paramters
89
+ def self.count(*args)
90
+ statement = TASK_BIN + ' count'
91
+ args.each do |param|
92
+ param.each do |attr, value|
93
+ statement << " #{attr.to_s}:#{value}"
94
+ end
95
+ end
96
+ return `#{statement}`.strip!
97
+ end
98
+
99
+ ###############################################
100
+ # CLASS METHODS FOR INTERACTING WITH TASKS
101
+ # (THESE WILL PROBABLY BECOME INSTANCE METHODS)
102
+ ###############################################
103
+
104
+ # Mark a task as complete
105
+ # TODO: Make into instance method when `task` supports finding by UUID.
106
+ def self.complete!(task_id)
107
+ statement = TASK_BIN + " #{task_id} done"
108
+ `#{statement}`
109
+ end
110
+
111
+ end
112
+
113
+ ###########################################
114
+ # UTILITY CLASS FOR DYNAMIC FINDER MATCHING
115
+ ###########################################
116
+ class TaskDynamicFinderMatch
117
+
118
+ attr_accessor :attribute
119
+
120
+ def initialize(method_sym)
121
+ if method_sym.to_s =~ /^find_by_(.*)$/
122
+ @attribute = $1
123
+ end
124
+ end
125
+
126
+ def match?
127
+ @attribute != nil
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ module TaskwarriorWeb
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubygems'
5
+
6
+ require 'taskwarrior-web/app'
7
+ require 'taskwarrior-web/task'
8
+ require 'taskwarrior-web/config'