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.
- data/.gitignore +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +6 -0
- data/Rakefile +2 -0
- data/bin/task-web +10 -0
- data/config.ru +9 -0
- data/lib/taskwarrior-web/app.rb +172 -0
- data/lib/taskwarrior-web/config.rb +11 -0
- data/lib/taskwarrior-web/task.rb +131 -0
- data/lib/taskwarrior-web/version.rb +3 -0
- data/lib/taskwarrior-web.rb +8 -0
- data/public/css/gh-buttons.css +388 -0
- data/public/css/jquery-ui.css +738 -0
- data/public/css/jquery.tagsinput.css +6 -0
- data/public/css/styles.css +283 -0
- data/public/css/tipsy.css +7 -0
- data/public/favicon.ico +0 -0
- data/public/images/ajax-loader.gif +0 -0
- data/public/images/arrow_right_black.png +0 -0
- data/public/images/arrow_right_grey.png +0 -0
- data/public/images/bg_fallback.png +0 -0
- data/public/images/gh-icons.png +0 -0
- data/public/images/grid-view.png +0 -0
- data/public/images/icon_sprite.png +0 -0
- data/public/images/list-view.png +0 -0
- data/public/images/logo.png +0 -0
- data/public/images/progress_bar.gif +0 -0
- data/public/images/slider_handles.png +0 -0
- data/public/images/subnav_background.gif +0 -0
- data/public/images/tab_background.gif +0 -0
- data/public/images/tipsy.gif +0 -0
- data/public/images/ui-icons_222222_256x240.png +0 -0
- data/public/images/ui-icons_454545_256x240.png +0 -0
- data/public/js/application.js +130 -0
- data/public/js/jquery-ui.min.js +163 -0
- data/public/js/jquery.cookie.js +96 -0
- data/public/js/jquery.min.js +16 -0
- data/public/js/jquery.tagsinput.js +218 -0
- data/public/js/jquery.tipsy.js +104 -0
- data/taskwarrior-web.gemspec +25 -0
- data/views/404.erb +3 -0
- data/views/_navigation.erb +21 -0
- data/views/layout.erb +47 -0
- data/views/listing.erb +29 -0
- data/views/project.erb +25 -0
- data/views/projects.erb +35 -0
- data/views/task_form.erb +28 -0
- 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
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/task-web
ADDED
data/config.ru
ADDED
@@ -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,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
|