taskwarrior-web 0.0.1

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