taskwarrior-web 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|