-
68 |
- Notes 70 |
-
71 |
-
72 |
74 |
2 | .:: . .:::::::::. :::. .,-:::::/ . : :::. :::. :::. 3 | ';;, ;; ;;;' ;;;`;;;;, `;;;,;;-'````' ;;,. ;;; ;;`;; `;;;;, `;;; 4 | '[[, [[, [[' [[[ [[[[[. '[[[[[ [[[[[[/ [[[[, ,[[[[, ,[[ '[[, [[[[[. '[[ 5 | Y$c$$$c$P $$$ $$$ "Y$c$$"$$c. "$$ $$$$$$$$"$$$c $$$cc$$$c $$$ "Y$c$$ 6 | "88"888 888 888 Y88 `Y8bo,,,o88o 888 Y88" 888o 888 888, 888 Y88 7 | "M "M" MMM MMM YM `'YMUP"YMM MMM M' "MMM YMM ""`. MMM YM 8 | 9 | An open source to-do list app 10 |11 | 12 | h3. News 13 | 14 | Twitter: "@alex_young":http://twitter.com/alex_young. 15 | 16 | * [2011-02-27] Updated to Rails 3.0.5, Mongoid 2.0.0.rc.1, added mongoid_session_store, empty search will show a message again 17 | * [2011-01-24] OpenID will be remember in a cookie 18 | * [2010-12-01] Various interface bug fixes 19 | * [2010-11-25] Added list to move task to a different project (so dragging isn't required). Keyboard shortcut is 'f' 20 | * [2010-11-17] Added 'Not Today' button 21 | * [2010-11-09] Default titles will be set instead of blank project names 22 | * [2010-11-06] shift-j and shift-k move through projects, return will mark as done, added text export to export project to-do lists 23 | * [2010-11-03] Task names and notes are now escaped, so pasting in HTML should be OK 24 | 25 | !http://github.com/alexyoung/wingman/raw/master/public/screenshots/wingman.png! 26 | 27 | This is an open source to-do list web application. It features a rich desktop-like interface. 28 | 29 | "Try the demo":http://wingman.heroku.com/ 30 | 31 | h3. Installation 32 | 33 | You'll need the following: 34 | 35 | # An account with an OpenID provider. These are easier to come by than you might think (if you use Flickr you have one through Yahoo!) 36 | # A mongo server. I use "MongoHQ":http://mongohq.com/ for some projects, but it's easy to install locally (with apt, homebrew, ports, etc.) 37 | # A web server for public use. Apache or Nginx with "Passenger":http://www.modrails.com/ will work great 38 | # This project will also work well with Heroku 39 | 40 | To install: 41 | 42 | # Check the project out with
git clone
43 | # Fill out your Mongo server details in config/mongoid.yml
44 | # Run bundle install
(prefix with sudo
if required)
45 | # Run rails server
or install for your web server
46 |
47 | h3. Heroku Configuration
48 |
49 | Run this to add the settings Mongo requires:
50 |
51 |
52 | heroku config:add MONGOID_HOST=server_hostname MONGOID_PORT=27039 MONGOID_DATABASE=database_name MONGOID_USERNAME=username MONGOID_PASSWORD=password
53 |
54 |
55 | h3. Libraries
56 |
57 | * Rails 3
58 | * ruby-openid, "documentation":http://openidenabled.com/files/ruby-openid/docs/2.1.2/, "example code":http://github.com/pelle/ruby-openid/blob/master/examples/rails_openid/app/controllers/consumer_controller.rb
59 | * mongoid, "documentation":http://mongoid.org/docs/installation/
60 | * jQuery, "jQueryUI":http://jqueryui.com
61 | * "Aristo jQuery theme":http://taitems.tumblr.com/post/482577430/introducing-aristo-a-jquery-ui-theme, "demo":http://www.warfuric.com/taitems/demo.html
62 | * json2
63 |
64 | h3. Assets
65 |
66 | * "OpenID badge":http://openid.net/foundation/news/logos/
67 | * Aristo theme graphics
68 |
69 | h3. To-do
70 |
71 | * The models should use embedded relationships
72 | * Improved mobile interface
73 |
74 | h3. License (GPL)
75 |
76 | This program is free software: you can redistribute it and/or modify
77 | it under the terms of the GNU General Public License as published by
78 | the Free Software Foundation, either version 3 of the License, or
79 | (at your option) any later version.
80 |
81 | This program is distributed in the hope that it will be useful,
82 | but WITHOUT ANY WARRANTY; without even the implied warranty of
83 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
84 | GNU General Public License for more details.
85 |
86 | You should have received a copy of the GNU General Public License
87 | along with this program. If not, see "http://www.gnu.org/licenses/":http://www.gnu.org/licenses/.
88 |
89 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 | require 'rake'
6 |
7 | Wingman::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | #protect_from_forgery
3 |
4 | def main
5 | load_user
6 | end
7 |
8 | def logout
9 | session.clear
10 | redirect_to '/'
11 | end
12 |
13 | def alljs
14 | render :text => Wingman.alljs
15 | end
16 |
17 | private
18 |
19 | def load_user
20 | @current_user = User.find :first, :conditions => { :identity_url => session[:identity_url] }
21 | end
22 |
23 | def requires_authentication
24 | if session[:identity_url] and load_user
25 | true
26 | else
27 | respond_to do |wants|
28 | wants.js { render :text => 'Access denied', :status => :unauthorized }
29 | wants.html { redirect_to new_openid_url }
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/controllers/openid_controller.rb:
--------------------------------------------------------------------------------
1 | require 'openid/store/memory'
2 | require 'openid/store/filesystem'
3 | require 'lib/db_store'
4 |
5 | class OpenidController < ApplicationController
6 | def index
7 | render :action => 'new'
8 | end
9 |
10 | def new
11 | end
12 |
13 | def create
14 | openid_response = openid_consumer.begin params[:openid_url]
15 | immediate = false
16 | return_to = url_for :action => 'complete', :only_path => false
17 | realm = url_for :action => 'index', :only_path => false
18 | cookies[:open_id] = params[:openid_url]
19 |
20 | if openid_response.send_redirect?(realm, return_to, immediate)
21 | redirect_to openid_response.redirect_url(realm, return_to, immediate)
22 | else
23 | render :text => openid_response.html_markup(realm, return_to, immediate, {'id' => 'openid_form'})
24 | end
25 | rescue OpenID::DiscoveryFailure
26 | flash[:error] = 'Error, please enter a valid OpenID URL'
27 | redirect_to '/'
28 | end
29 |
30 | def complete
31 | current_url = url_for :action => 'complete', :only_path => false
32 | parameters = params.reject { |k,v| request.path_parameters[k.to_sym] }
33 | openid_response = openid_consumer.complete(parameters, current_url)
34 | case openid_response.status
35 | when OpenID::Consumer::FAILURE
36 | if openid_response.display_identifier
37 | flash[:error] = "Verification of #{openid_response.display_identifier} failed: #{openid_response.message}"
38 | else
39 | flash[:error] = "Verification failed: #{openid_response.message}"
40 | end
41 | when OpenID::Consumer::SUCCESS
42 | session[:identity_url] = openid_response.identity_url
43 |
44 | unless User.find :first, :conditions => { :identity_url => openid_response.identity_url }
45 | User.create :identity_url => openid_response.identity_url, :display_identifier => openid_response.display_identifier
46 | end
47 |
48 | flash[:info] = "Verification of #{openid_response.display_identifier} succeeded."
49 | when OpenID::Consumer::SETUP_NEEDED
50 | flash[:error] = "Immediate request failed - Setup needed"
51 | when OpenID::Consumer::CANCEL
52 | flash[:error] = "OpenID transaction cancelled."
53 | else
54 | flash[:error] = "OpenID login failed."
55 | end
56 | redirect_to '/'
57 | end
58 |
59 | private
60 |
61 | def openid_store
62 | OpenID::Store::DbStore.new
63 | end
64 |
65 | def openid_consumer
66 | if @openid_consumer.nil?
67 | @openid_consumer = OpenID::Consumer.new(session, openid_store)
68 | end
69 | return @openid_consumer
70 | end
71 |
72 | end
73 |
74 |
--------------------------------------------------------------------------------
/app/controllers/storage_controller.rb:
--------------------------------------------------------------------------------
1 | class StorageController < ApplicationController
2 | before_filter :requires_authentication
3 |
4 | # The entire collection of data
5 | def restore
6 | data = {
7 | 'collections' => kv_hash('collections'),
8 | 'settings' => kv_hash('settings'),
9 | 'projects' => collection_hash('projects'),
10 | 'tasks' => collection_hash('tasks')
11 | }
12 |
13 | render :json => data
14 | end
15 |
16 | # GET /storage/archive
17 | def archive
18 | # TODO: Pagination
19 | @tasks = Task.where(:user_id => @current_user.id).and(:archived => true).desc(:created_at)
20 | render :json => @tasks
21 | end
22 |
23 | # POST /storage
24 | def create
25 | json = JSON.parse params[:data]
26 | json['user_id'] = @current_user.id
27 |
28 | save_and_respond do
29 | collection_class(params[:collection]).create json
30 | end
31 | end
32 |
33 | # PUT /storage
34 | def update
35 | json = JSON.parse params[:data]
36 | json['user_id'] = @current_user.id
37 | item = collection_class(params[:collection]).find(
38 | :first,
39 | :conditions => {
40 | :user_id => @current_user.id,
41 | :id => json['id']
42 | }
43 | )
44 |
45 | if item.nil?
46 | render :json => { 'error' => "Couldn't find #{params[:collection]} with ID: #{json['id']}" }, :status => :error
47 | else
48 | save_and_respond do
49 | item.update_attributes json
50 | item
51 | end
52 | end
53 | end
54 |
55 | # PUT /storage/set_key_value
56 | def set_key_value
57 | json = JSON.parse params['data']
58 | key = json['key']
59 | value = json['value']
60 | item = collection_class(params['collection']).find(:all, :conditions => { :user_id => @current_user.id, :key => key })[0]
61 |
62 | save_and_respond do
63 | if item.nil?
64 | item = collection_class(params['collection']).create({ :key => key, :value => value, :user_id => @current_user.id })
65 | else
66 | item.update_attributes :value => value
67 | end
68 | item
69 | end
70 | end
71 |
72 | # DELETE /storage
73 | def destroy
74 | item = collection_class(params[:collection]).find(
75 | :first,
76 | :conditions => {
77 | :user_id => @current_user.id,
78 | :id => params[:id]
79 | }
80 | )
81 |
82 | save_and_respond do
83 | item.destroy
84 | item
85 | end
86 | end
87 |
88 | def update_user
89 | @current_user.update_attributes :name => params[:current_user][:name]
90 | if @current_user.valid?
91 | render :text => 'Your name has been changed'
92 | else
93 | render :text => @current_user.errors.full_messages.to_sentence, :status => :error
94 | end
95 | end
96 |
97 | private
98 | include Wingman::HashHelpers
99 |
100 | def save_and_respond(&block)
101 | respond_to do |format|
102 | item = yield
103 | if item.valid?
104 | format.json { head :ok, :status => :success }
105 | else
106 | format.json { render :json => item.errors, :status => :unprocessable_entity }
107 | end
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def logged_in?
3 | session[:identity_url]
4 | end
5 |
6 | def render_flash
7 | flash.map do |flash_type, message|
8 | display_flash flash_type, message
9 | end.compact.join("\n")
10 | end
11 |
12 | def display_flash(flash_type, message)
13 | html =<<-HTML
14 |
20 | HTML
21 | end
22 |
23 | def flash_icon(flash_type)
24 | case flash_type
25 | when :error
26 | 'ui-icon-alert'
27 | when :info, :sucess
28 | 'ui-icon-info'
29 | else
30 | ''
31 | end
32 | end
33 |
34 | def flash_class(flash_type)
35 | case flash_type
36 | when :info, :sucess
37 | 'highlight'
38 | else
39 | flash_type
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/helpers/storage_helper.rb:
--------------------------------------------------------------------------------
1 | module StorageHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/javascripts/application.js:
--------------------------------------------------------------------------------
1 | var dateFormat = 'yy/mm/dd',
2 | originalEditableValue,
3 | dragLock = new Lock(),
4 | userAgent,
5 | userAgentFamily;
6 |
7 | if (navigator.userAgent.match(/iPad/i) != null) {
8 | userAgent = 'iPad';
9 | userAgentFamily = 'iOS';
10 | } else if (navigator.userAgent.match(/iPhone/i) != null) {
11 | userAgent = 'iPhone';
12 | userAgentFamily = 'iOS';
13 | }
14 |
15 | $(document).ajaxError(function(e, xhr, settings, exception) {
16 | Storage.done();
17 | if (xhr.status !== 200) {
18 | if (settings.url.match(/update_openid/)) {
19 | $('#settings-feedback').html(Feedback.message('error', xhr.responseText));
20 | } else if (xhr.status === 401) {
21 | window.location = '/logout';
22 | } else {
23 | console.log('error in: ' + settings.url + ' \n' + 'error: ' + xhr.responseText);
24 | }
25 | }
26 | });
27 |
28 | Storage.ready = function() {
29 | generateExampleData();
30 | ProjectsController.displayAll();
31 | resize();
32 | Feedback.hide();
33 | };
34 |
35 | Storage.loading = function() {
36 | $('#loading-indicator').show();
37 | };
38 |
39 | Storage.done = function() {
40 | setTimeout(function() { $('#loading-indicator').hide('fade', {}, 250) }, 500);
41 | };
42 |
43 | function generateExampleData() {
44 | if (Project.findAll().length === 0) {
45 | var p = Project.create({ name: 'Project ' + 1, tags: 'tag1 tag2 tag3', notes: null });
46 | Task.create({ 'project_id': p.id, name: 'Example task', done: false, archived: false });
47 | var t = Task.create({ 'project_id': null, name: 'Example task to do today', done: false, archived: false });
48 | Collection.set('inbox', []);
49 | Collection.set('today', [t.get('id')]);
50 | Collection.set('next', []);
51 | }
52 | }
53 |
54 | if ($('#login-dialog').length === 0) {
55 | Feedback.info('Loading...');
56 | Storage.remote.read();
57 | } else {
58 | $('#OpenIDHelpLink').click(function() {
59 | $('.open-id-help').toggle();
60 | });
61 | }
62 |
63 | // Correct widths and heights based on window size
64 | function resize() {
65 | var height = $(window).height() - $('#global-menu').height() - 11, containerWidth = $($('ul.project-header')[0]).width(),
66 | width = $('.content').width() - $('.ui-icon-todo').width() - $('.ui-icon-trash').width() - 88 + 'px';
67 |
68 | $('.outline-view').css({ height: height + 'px' });
69 | $('.content').css({ height: height + 'px', width: $('body').width() - $('.outline-view').width() - $('.content-divider').width() - 1 + 'px' });
70 | $('.content-divider').css({ height: height + 'px' });
71 |
72 | if (!containerWidth) {
73 | containerWidth = $('.content').width();
74 | }
75 |
76 | $('.todo-items .button').each(function() {
77 | this.style.width = containerWidth - $($(this).prev('.state')[0]).width() - 22 + 'px';
78 | });
79 | $('.name-text').css({ width: width, 'max-width': parseInt(width, 10) - 50 });
80 | }
81 |
82 | $(window).resize(function() {
83 | setTimeout(resize, 100);
84 | });
85 |
86 | $(window).focus(resize);
87 |
88 | function selectedCollectionIsNamed() {
89 | return $('.outline-view .items li.selected a').hasClass('named-collection');
90 | }
91 |
92 | function selectedProject() {
93 | return $('.outline-view .items.projects li.selected a').itemID();
94 | }
95 |
96 | function selectProject() {
97 | var setting = Settings.get('outline-view');
98 | if (setting && setting.length > 0) {
99 | $(setting).trigger('click');
100 | }
101 |
102 | if ($('.outline-view .selected').length === 0) {
103 | $('.outline-view .items.projects li a').first().trigger('click');
104 | }
105 |
106 | if ($('.outline-view .selected').length === 0) {
107 | $('#show-today').trigger('click');
108 | }
109 | }
110 |
111 | function hideArchiveButtonIfRequired() {
112 | if ($('.todo-items .done').length > 0 && $('ul.archive li.selected').length === 0) {
113 | $('#archive-tasks').closest('li').show();
114 | } else {
115 | $('#archive-tasks').closest('li').hide();
116 | }
117 | }
118 |
119 | // Outline view
120 | $('.outline-view ul.items a').live('click', function() {
121 | if (dragLock.locked) return;
122 |
123 | var element = $(this), selectedItem;
124 | element.closest('.outline-view').find('li.selected').removeClass('selected');
125 | element.parent().toggleClass('selected');
126 |
127 | if (element.closest('ul').hasClass('projects')) {
128 | ProjectsController.display(Project.find(element.itemID()), element);
129 | }
130 |
131 | selectedItem = '#' + element.attr('id');
132 | if (Settings.get('outline-view') !== selectedItem) {
133 | Settings.set('outline-view', selectedItem);
134 | }
135 | closeEditable();
136 | });
137 |
138 | // Sections
139 | $('#show-settings').click(function() {
140 | $('#settings-feedback').html('');
141 | $('.content').hide();
142 | $('#settings').show();
143 | $('.task-related-button').hide();
144 | $('.settings-related-button').show();
145 | $('.outline-view .selected').removeClass('selected');
146 | });
147 |
148 | $('.outline-view a').live('click', function() {
149 | if (dragLock.locked) return;
150 |
151 | TasksController.removeArchived();
152 | $('.content').hide();
153 | $('#project').show();
154 | $('.task-related-button').show();
155 | $('.settings-related-button').hide();
156 |
157 | $('#project-todo-items').addClass('todo-items');
158 | $('#search-todo-items').removeClass('todo-items');
159 | $('#search input').val(defaultFieldValues.search);
160 |
161 | resize();
162 | $('#delete-task').closest('li').hide();
163 | $('#not-today').closest('li').hide();
164 | hideArchiveButtonIfRequired();
165 | });
166 |
167 | $('.named-collection').click(function() {
168 | if (dragLock.locked) return;
169 |
170 | var collectionName = $(this).attr('id').split('-')[1],
171 | displayOptions = {};
172 |
173 | $('.outline-view .selected').removeClass('selected');
174 | $(this).closest('li').addClass('selected');
175 | $('#project').show();
176 |
177 | if (selectedCollectionIsNamed()) {
178 | displayOptions = { show_projects: true };
179 | }
180 |
181 | TasksController.display(jQuery.map(Collection.get(collectionName) || [], function(value) {
182 | return Task.find(value);
183 | }), displayOptions);
184 | $('.project-field').hide();
185 | });
186 |
187 | $('#show-archive').click(function() {
188 | if (dragLock.locked) return;
189 |
190 | $('.project-field').hide();
191 | TasksController.clear();
192 | $('#project').show();
193 |
194 | Feedback.info('Loading...');
195 |
196 | // TODO: Paginate
197 | jQuery.getJSON('/storage/archive', function(data) {
198 | Feedback.hide();
199 | var tasks = [];
200 | jQuery.each(data, function() {
201 | this.id = this._id;
202 | Storage.data.tasks[this.id] = this;
203 | tasks.push(Task.find(this.id));
204 | });
205 | if (tasks && tasks.length > 0) {
206 | TasksController.display(tasks, { show_projects: true });
207 | } else {
208 | Feedback.info('No tasks have been archived.');
209 | }
210 | });
211 | });
212 |
213 | // State change (done)
214 | $('.project-field .state').live('click', function() {
215 | $('.todo-items .state').each(function() {
216 | var element = $(this);
217 | if (!element.hasClass('done')) {
218 | element.trigger('click');
219 | }
220 | });
221 |
222 | var project = Project.find(selectedProject());
223 | project.set('done', true);
224 | ProjectsController.displayState(project);
225 | });
226 |
227 | $('#delete-task').click(function() {
228 | TasksController.destroy($('.todo-items .highlight').closest('li'));
229 | TasksController.destroy($('.todo-items li.task.details'));
230 | $('#delete-task').closest('li').hide();
231 | });
232 |
233 | $('#archive-tasks').click(function() {
234 | TasksController.archive($('.todo-items .done').parents('li.task'));
235 | });
236 |
237 | $('#not-today').click(function() {
238 | $('#not-today').closest('li').hide();
239 | TasksController.notToday($('.todo-items .highlight').closest('li'));
240 | });
241 |
242 | function parseDate(value) {
243 | if (!value) return (new Date());
244 | return $.datepicker.parseDate(dateFormat, value, {});
245 | }
246 |
247 | function presentDate(value) {
248 | if (!value) return;
249 | return $.datepicker.formatDate($.datepicker.RFC_2822, $.datepicker.parseDate(dateFormat, value, {}));
250 | }
251 |
252 | function datePickerSave(value, element, picker) {
253 | var d = $.datepicker.parseDate('mm/dd/yy', value, {}),
254 | container;
255 | element.html(' '
256 | + ' ');
257 | $(picker).remove();
258 |
259 | // Save
260 | container = element.closest('li.task');
261 | if (container.length > 0) {
262 | Task.find(container.itemID()).set('due', $.datepicker.formatDate(dateFormat, d));
263 | } else {
264 | Project.find(selectedProject()).set('due', $.datepicker.formatDate(dateFormat, d));
265 | }
266 | }
267 |
268 | function escapeQuotes(text) {
269 | return text ? text.replace(/"/g, '"') : text;
270 | }
271 |
272 | $('.editable-field').live('click', function(e) {
273 | var element = $(this),
274 | content,
275 | datePicker,
276 | closestDate,
277 | input,
278 | container = element.closest('li.task');
279 |
280 | if (e.target.nodeName === 'INPUT' || e.target.nodeName === 'FORM') return true;
281 | if (element.find('form').length > 0) return true;
282 |
283 | closeEditable(element);
284 |
285 | if (element.find('form').length === 0) {
286 | if (element.hasClass('type-date')) {
287 | if (container.length > 0) {
288 | closestDate = Task.find(container.itemID()).get('due');
289 | } else {
290 | closestDate = Project.find(selectedProject()).get('due');
291 | }
292 |
293 | $('.content').first().append('');
294 | datePicker = $('#datepicker').datepicker({ autoSize: true, onSelect: function(value) { datePickerSave(value, element, this); }, defaultDate: parseDate(closestDate) });
295 | datePicker.css({ 'position': 'absolute', 'z-index': 99, 'left': element.offset().left, 'top': element.offset().top });
296 | } else {
297 | try {
298 | if (content === defaultFieldValues[element.attr('name')]) {
299 | content = '';
300 | } else if (container.itemID()) {
301 | content = Task.find(container.itemID()).get(element.attr('name'));
302 | } else {
303 | var projectFieldName = element.attr('name').split(/project_/),
304 | projectID = $('.outline-view li.selected a').itemID();
305 | content = Project.find(projectID).get(projectFieldName[1]);
306 | }
307 |
308 | if (!content) content = '';
309 |
310 | if (element.hasClass('large')) {
311 | input = '';
312 | } else {
313 | input = '';
314 | }
315 | element.html('').find('.field').trigger('focus');
316 | originalEditableValue = content;
317 | } catch (exception) {
318 | console.log(exception);
319 | }
320 | }
321 | }
322 | });
323 |
324 | $('.clear-due').live('click', function(e) {
325 | var element = $(this);
326 | var task = Task.find(element.closest('.task').itemID());
327 | task.set('due', null);
328 | element.closest('li').html(defaultFieldValues.due);
329 | element.remove();
330 | e.preventDefault();
331 | return false;
332 | });
333 |
334 | if (userAgentFamily != 'iOS') {
335 | $('.editable .field').live('blur', function(e) {
336 | saveEditable();
337 | closeEditable();
338 | });
339 | }
340 |
341 | if (userAgentFamily !== 'iOS') {
342 | $('.content').live('click', function(e) {
343 | if ($(e.target).hasClass('content')) {
344 | TasksController.closeEditors();
345 | $('.todo-items .highlight').removeClass('highlight');
346 | }
347 | });
348 | }
349 |
350 | // Delete project dialog
351 | $('#delete-project-dialog').dialog({
352 | autoOpen: false,
353 | width: 600,
354 | buttons: {
355 | 'OK': function() {
356 | $(this).dialog('close');
357 | Project.destroy(selectedProject());
358 | ProjectsController.displayAll();
359 | $('a.named-collection').first().click();
360 | },
361 | 'Cancel': function() {
362 | $(this).dialog('close');
363 | }
364 | },
365 | modal: true
366 | });
367 |
368 | $('#export-text-dialog').dialog({
369 | autoOpen: false,
370 | width: 600,
371 | buttons: {
372 | 'OK': function() {
373 | $(this).dialog('close');
374 | },
375 | },
376 | modal: true
377 | });
378 |
379 | // Modal login panel
380 | $('#login-dialog').dialog({
381 | autoOpen: true,
382 | title: 'Please Login',
383 | width: 400,
384 | modal: true,
385 | closeOnEscape: false,
386 | beforeclose: function() { return false; }
387 | });
388 | $('#login-button').button({ });
389 | $('#login-button').click(function() { $(this).closest('form').submit(); });
390 | $('#openid_url').select();
391 |
392 | // Resize when the dialog opens/closes else it sometimes messes up the scrollbars
393 | $('#delete-project-button').click(function(e) {
394 | $('#delete-project-dialog').dialog('open');
395 | resize();
396 | e.preventDefault();
397 | return false;
398 | });
399 |
400 | $('#export-text-button').click(function(e) {
401 | $('#export-text-dialog').dialog('open');
402 | var input = $('#export-text-value'),
403 | project = Project.find(selectedProject()),
404 | tasks = ProjectsController.tasks(project),
405 | output = '',
406 | done;
407 |
408 | for (var i in tasks) {
409 | done = tasks[i].get('done') ? '✓ ' : '◻ ';
410 | output += done + tasks[i].get('name') + '\n';
411 | }
412 | input.html(output);
413 | e.preventDefault();
414 | });
415 |
416 | $(document).bind('dialogclose', function(event, ui) {
417 | resize();
418 | });
419 |
420 | $('.state').live('mouseenter', function() { $(this).addClass('ui-state-hover'); });
421 | $('.state').live('mouseleave', function() { $(this).removeClass('ui-state-hover'); });
422 |
423 | $('.delete').live('mouseenter', function() { $(this).addClass('ui-state-hover'); });
424 | $('.delete').live('mouseleave', function() { $(this).removeClass('ui-state-hover'); });
425 |
426 | $('.outline-view ul.items li').live('mouseenter', function() {
427 | $(this).addClass('hover');
428 | });
429 |
430 | $('.outline-view ul.items li').live('mouseleave', function() {
431 | $(this).removeClass('hover');
432 | });
433 |
434 | // Resizable panes
435 | (function() {
436 | var moving = false, width = 0;
437 |
438 | function start() {
439 | moving = true;
440 | }
441 |
442 | function end() {
443 | if (width > 0) {
444 | Settings.set('outline-view-width', width);
445 | }
446 | moving = false;
447 | }
448 |
449 | function move(e) {
450 | if (moving) {
451 | $('.outline-view').css({ width: e.pageX });
452 | width = e.pageX;
453 | resize();
454 | }
455 | }
456 |
457 | $('.content-divider').bind('mousedown', start);
458 | $(document).bind('mousemove', move);
459 | $(document).bind('mouseup', end);
460 | })();
461 |
462 | // Setup
463 | $('.todo-items .button').button({});
464 | $('.add-button').button({ icons: { primary: 'ui-icon-circle-plus' } });
465 | $('#delete-task').button({ icons: { primary: 'ui-icon-trash' } });
466 | $('#archive-tasks').button({ icons: { primary: 'ui-icon-arrowreturnthick-1-e' } });
467 | $('#not-today').button({ icons: { primary: 'ui-icon-arrowreturnthick-1-e' } });
468 | $('#show-settings').button({ icons: { primary: 'ui-icon-gear' } });
469 | $('#logout').button({ icons: { primary: 'ui-icon-power' } });
470 | $('#search').button({ icons: { primary: 'ui-icon-search' } });
471 |
472 | // disableTextSelect works better than disableSelection
473 | $('.content-divider').disableTextSelect();
474 |
475 | $('.todo-items .done').addClass('ui-state-disabled');
476 |
477 | $('#project-todo-items').sortable({
478 | handle: '.handle',
479 | stop: function(e, ui) { dragLock.unlock(); TasksController.saveSort(e, ui); },
480 | revert: true,
481 | start: function(e, ui) { dragLock.lock(); $(ui.item).addClass('dragging'); }
482 | }).disableSelection();
483 | $('.outline-view ul .projects').sortable({ stop: ProjectsController.saveSort }).disableSelection();
484 |
485 | $('#tabs').tabs();
486 |
487 | // Today droppable
488 | $('#show-today').droppable({
489 | hoverClass: 'hover-drag',
490 | dragClass: 'dragging',
491 | accept: '.task',
492 | drop: function(e, ui) {
493 | var taskElement = $(e.srcElement).closest('.task'),
494 | task = Task.find(taskElement.itemID());
495 |
496 | // Add to today
497 | if (task) {
498 | Collection.appendItem('today', task.get('id'));
499 | taskElement.find('.ui-icon-todo').addClass('ui-icon-todo-today');
500 |
501 | // Remove from inbox
502 | Collection.removeItem('inbox', task.get('id'));
503 |
504 | if ($('#show-inbox').closest('li').hasClass('selected')) {
505 | taskElement.remove();
506 | }
507 | }
508 |
509 | dragLock.timedUnlock();
510 | }
511 | });
512 |
513 | $('#show-inbox').droppable({
514 | hoverClass: 'hover-drag',
515 | dragClass: 'dragging',
516 | accept: '.task',
517 | drop: function(e, ui) {
518 | var taskElement = $(e.srcElement).closest('.task'),
519 | task = Task.find(taskElement.itemID()),
520 | projectCollection;
521 |
522 | // Add to inbox
523 | if (task) {
524 | // Remove from today/projects
525 | Collection.removeItem('today', task.get('id'));
526 |
527 | if (task.get('project_id')) {
528 | Collection.removeItem('project_tasks_' + task.get('project_id'), task.get('id'));
529 | }
530 |
531 | // Add to inbox
532 | Collection.appendItem('inbox', task.get('id'));
533 | task.set('project_id', null);
534 |
535 | if (taskElement) taskElement.remove();
536 | }
537 |
538 | dragLock.timedUnlock();
539 | }
540 | });
541 |
542 | // This is used by the settings form
543 | $('form.settings_form').submit(function(e) {
544 | $('#settings-feedback').html('');
545 | var target = $(e.target);
546 | jQuery.post(target.attr('action'), target.serialize(), function() {
547 | $('#settings-feedback').html(Feedback.message('info', 'Your details have been changed'));
548 | });
549 | e.preventDefault();
550 | return false;
551 | });
552 |
553 | $('.project-field').hide();
554 | setTimeout(resize, 200);
555 |
--------------------------------------------------------------------------------
/app/javascripts/defaults.js:
--------------------------------------------------------------------------------
1 | var defaultFieldValues = {
2 | name: 'Untitled task',
3 | notes: 'Notes',
4 | due: 'Due Date',
5 | search: 'Search',
6 | project_name: 'Untitled',
7 | project_notes: 'Notes'
8 | };
9 |
10 |
--------------------------------------------------------------------------------
/app/javascripts/editable.js:
--------------------------------------------------------------------------------
1 | function closeEditable(element) {
2 | $('.editable-field form.editable').each(function() {
3 | var form = $(this),
4 | value = form.find('.field').val(),
5 | container = form.parent();
6 | if (!value || value.length === 0) {
7 | value = defaultFieldValues[container.attr('name')];
8 | }
9 |
10 | // Add 'project: ' to the task name
11 | if (selectedCollectionIsNamed()) {
12 | var task = Task.find(form.parents('.task').itemID()),
13 | project = Project.find(task.get('project_id'));
14 | if (project) {
15 | value = project.get('name') + ': ' + value;
16 | }
17 | }
18 |
19 | // I sometimes get NOT_FOUND_ERR: DOM Exception 8 if I don't do html('')
20 | container.html('').text(value);
21 | });
22 | $('#datepicker').remove();
23 | }
24 |
25 | function saveEditable() {
26 | var input = $('.editable .field').first(),
27 | projectID,
28 | taskID,
29 | task,
30 | project;
31 |
32 | if (input.length === 0) return;
33 | projectID = $('.outline-view li.selected a').itemID();
34 |
35 | if (projectID && input.closest('.task').length === 0) {
36 | project = Project.find(projectID);
37 | jQuery.each(['name', 'notes'], function(index, field) {
38 | if (input.closest('.' + field).length > 0) {
39 | project.set(field, input.val());
40 | }
41 | });
42 | ProjectsController.displayAll();
43 | } else {
44 | taskID = input.closest('li.task').itemID();
45 | if (taskID) {
46 | task = Task.find(taskID);
47 | jQuery.each(['name', 'tags', 'notes'], function(index, field) {
48 | if (input.closest('.' + field).length > 0) {
49 | task.set(field, input.val());
50 | }
51 | });
52 | }
53 | }
54 | }
55 |
56 | // FIXME: I don't understand the cause of this
57 | // browser fix -- the columns break when pasting content
58 | // Seen in Chrome and Safari
59 | $('form.editable textarea').live('paste', function(e) {
60 | setTimeout(resize, 5);
61 | setTimeout(resize, 10);
62 | });
63 |
64 | $('form.editable').live('submit', function(e) {
65 | e.preventDefault();
66 | saveEditable();
67 | closeEditable();
68 | });
69 |
70 |
--------------------------------------------------------------------------------
/app/javascripts/intro.js:
--------------------------------------------------------------------------------
1 | jQuery(document).ready(function() {
2 |
3 |
--------------------------------------------------------------------------------
/app/javascripts/jquery-extensions.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | function isScrolledIntoView(elem) {
3 | var documentTop = $(window).scrollTop();
4 | var documentBottom = documentTop + $(window).height() - $('#global-menu').height();
5 |
6 | var elementTop = $(elem).offset().top;
7 | var elementBottom = elementTop + $(elem).height();
8 |
9 | return ((elementBottom >= documentTop) && (elementTop <= documentBottom)
10 | && (elementBottom <= documentBottom) && (elementTop >= documentTop));
11 | }
12 |
13 | $.fn.highlight = function() {
14 | var element = $(this),
15 | scrollContainer = $('#project');
16 | element.addClass('highlight');
17 | if (!isScrolledIntoView(element)) {
18 | scrollContainer.animate({ scrollTop: element.position().top }, 250);
19 | }
20 | };
21 |
22 | $.fn.escapeText = function(text) {
23 | if (text) {
24 | return $('').text(text).html();
25 | }
26 | };
27 |
28 | $.fn.disableTextSelect = function() {
29 | return this.each(function() {
30 | if ($.browser.mozilla) {
31 | $(this).css('MozUserSelect', 'none');
32 | } else if ($.browser.msie) {
33 | $(this).bind('selectstart', function() { return false; });
34 | } else {
35 | $(this).mousedown(function() { return false; });
36 | }
37 | });
38 | };
39 |
40 | $.fn.enableTextSelect = function() {
41 | return this.each(function() {
42 | if ($.browser.mozilla) {
43 | $(this).css('MozUserSelect', 'text');
44 | } else if ($.browser.msie) {
45 | $(this).bind('selectstart', function() { return true; });
46 | } else {
47 | $(this).mousedown(function() { return true; });
48 | }
49 | });
50 | };
51 |
52 | $.fn.itemID = function() {
53 | try {
54 | return $(this).attr('id').match(/_(\d+)/)[1];
55 | } catch (exception) {
56 | return null;
57 | }
58 | };
59 | })(jQuery);
60 |
61 |
--------------------------------------------------------------------------------
/app/javascripts/keyboard.js:
--------------------------------------------------------------------------------
1 | function initKeyboardHandlers() {
2 | var nextResponder = $();
3 |
4 | function saveField(e) {
5 | saveEditable();
6 | closeEditable();
7 | }
8 |
9 | $('.todo-items :input').live('focus', function() {
10 | nextResponder = $(this).closest('li').next();
11 | });
12 |
13 | $(document).bind('keyup', function(e) {
14 | if (e.which === 27) {
15 | // [esc]
16 | if ($('.task.details').length > 0
17 | && $('.editable .field').length === 0) {
18 | var task = $('.task.details');
19 | TasksController.closeEditors();
20 | task.find('.button').click();
21 | } else if ($('.editable .field').length > 0) {
22 | closeEditable();
23 | nextResponder = null;
24 | } else if ($('.task .highlight').length > 0) {
25 | $('#project').click();
26 | }
27 | }
28 |
29 | if (e.which === 9) {
30 | // Tab
31 | if ($('#sort-dialog').is(':visible')) return;
32 | if ($('.editable .field').length > 0) saveField(e);
33 | e.preventDefault();
34 | if (!nextResponder) nextResponder = $('ul.details li.first');
35 |
36 | if (nextResponder.hasClass('last')) {
37 | nextResponder.click();
38 | nextResponder = nextResponder.closest('ul').find('li.first');
39 | } else {
40 | nextResponder.click();
41 | }
42 |
43 | return false;
44 | }
45 | });
46 |
47 | $(document).bind('keypress', function(e) {
48 | if ($(e.target).is(':input')) return;
49 |
50 | if (e.which === 35) {
51 | // # for delete
52 | var selectedTask = $('.task .highlight').first();
53 | if (selectedTask.length > 0) {
54 | $('.delete-button').click();
55 | e.preventDefault();
56 | return;
57 | }
58 | }
59 |
60 | if (e.which === 74) {
61 | // J
62 | var navItems = $('.outline-view li a'),
63 | selected = $('.outline-view li.selected a').first(),
64 | i = navItems.index(selected);
65 | $(navItems[i == navItems.length - 1 ? 0 : i + 1]).click();
66 | e.preventDefault();
67 | return;
68 | } else if (e.which === 75) {
69 | // K
70 | var navItems = $('.outline-view li a'),
71 | selected = $('.outline-view li.selected a').first(),
72 | i = navItems.index(selected);
73 | $(navItems[i == 0 ? navItems.length - 1 : i - 1]).click();
74 | e.preventDefault();
75 | return;
76 | }
77 |
78 | if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
79 |
80 | if (e.which === 106) {
81 | // j
82 | TasksController.closeEditors();
83 | var selectedTask = $('.task .highlight').first(),
84 | nextElement = $('.task .highlight').closest('li').next().find('.button');
85 | if (selectedTask.length === 0) {
86 | $('li.body li.task .button').first().click();
87 | } else if (nextElement.length > 0) {
88 | nextElement.click();
89 | } else {
90 | $('li.body li.task .button').first().click();
91 | }
92 | } else if (e.which === 107) {
93 | // k
94 | TasksController.closeEditors();
95 | var selectedTask = $('.task .highlight').first(),
96 | prevElement = $('.task .highlight').closest('li').prev().find('.button');
97 | if (selectedTask.length === 0) {
98 | $('li.body li.task .button').last().click();
99 | e.preventDefault();
100 | } else if (prevElement.length > 0) {
101 | prevElement.click();
102 | e.preventDefault();
103 | } else {
104 | $('li.body li.task .button').last().click();
105 | e.preventDefault();
106 | }
107 | } else if ((e.which === 32 || e.which === 79 || e.which === 111)
108 | && $('.task .highlight').length > 0) {
109 | // space or 'o'
110 | TasksController.open($('.task .highlight').closest('.button'));
111 | e.preventDefault();
112 | } else if (e.which === 110) {
113 | $('.task.add-button').click();
114 | e.preventDefault();
115 | } else if (e.which === 121) {
116 | $('#archive-tasks').click();
117 | } else if (e.which === 102) {
118 | // 'f' - folder menu
119 | $('.sort-task').click();
120 | } else if (e.which === 13) {
121 | var selectedTask = $('.task .highlight');
122 | if (selectedTask.length > 0) {
123 | selectedTask.prev('.state').click();
124 | e.preventDefault();
125 | return;
126 | }
127 | }
128 | });
129 | }
130 |
131 | initKeyboardHandlers();
132 |
133 |
--------------------------------------------------------------------------------
/app/javascripts/lib/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | http://www.JSON.org/json2.js
3 | 2010-03-20
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, strict: false */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | if (!this.JSON) {
163 | this.JSON = {};
164 | }
165 |
166 | (function () {
167 |
168 | function f(n) {
169 | // Format integers to have at least two digits.
170 | return n < 10 ? '0' + n : n;
171 | }
172 |
173 | if (typeof Date.prototype.toJSON !== 'function') {
174 |
175 | Date.prototype.toJSON = function (key) {
176 |
177 | return isFinite(this.valueOf()) ?
178 | this.getUTCFullYear() + '-' +
179 | f(this.getUTCMonth() + 1) + '-' +
180 | f(this.getUTCDate()) + 'T' +
181 | f(this.getUTCHours()) + ':' +
182 | f(this.getUTCMinutes()) + ':' +
183 | f(this.getUTCSeconds()) + 'Z' : null;
184 | };
185 |
186 | String.prototype.toJSON =
187 | Number.prototype.toJSON =
188 | Boolean.prototype.toJSON = function (key) {
189 | return this.valueOf();
190 | };
191 | }
192 |
193 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
194 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
195 | gap,
196 | indent,
197 | meta = { // table of character substitutions
198 | '\b': '\\b',
199 | '\t': '\\t',
200 | '\n': '\\n',
201 | '\f': '\\f',
202 | '\r': '\\r',
203 | '"' : '\\"',
204 | '\\': '\\\\'
205 | },
206 | rep;
207 |
208 |
209 | function quote(string) {
210 |
211 | // If the string contains no control characters, no quote characters, and no
212 | // backslash characters, then we can safely slap some quotes around it.
213 | // Otherwise we must also replace the offending characters with safe escape
214 | // sequences.
215 |
216 | escapable.lastIndex = 0;
217 | return escapable.test(string) ?
218 | '"' + string.replace(escapable, function (a) {
219 | var c = meta[a];
220 | return typeof c === 'string' ? c :
221 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
222 | }) + '"' :
223 | '"' + string + '"';
224 | }
225 |
226 |
227 | function str(key, holder) {
228 |
229 | // Produce a string from holder[key].
230 |
231 | var i, // The loop counter.
232 | k, // The member key.
233 | v, // The member value.
234 | length,
235 | mind = gap,
236 | partial,
237 | value = holder[key];
238 |
239 | // If the value has a toJSON method, call it to obtain a replacement value.
240 |
241 | if (value && typeof value === 'object' &&
242 | typeof value.toJSON === 'function') {
243 | value = value.toJSON(key);
244 | }
245 |
246 | // If we were called with a replacer function, then call the replacer to
247 | // obtain a replacement value.
248 |
249 | if (typeof rep === 'function') {
250 | value = rep.call(holder, key, value);
251 | }
252 |
253 | // What happens next depends on the value's type.
254 |
255 | switch (typeof value) {
256 | case 'string':
257 | return quote(value);
258 |
259 | case 'number':
260 |
261 | // JSON numbers must be finite. Encode non-finite numbers as null.
262 |
263 | return isFinite(value) ? String(value) : 'null';
264 |
265 | case 'boolean':
266 | case 'null':
267 |
268 | // If the value is a boolean or null, convert it to a string. Note:
269 | // typeof null does not produce 'null'. The case is included here in
270 | // the remote chance that this gets fixed someday.
271 |
272 | return String(value);
273 |
274 | // If the type is 'object', we might be dealing with an object or an array or
275 | // null.
276 |
277 | case 'object':
278 |
279 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
280 | // so watch out for that case.
281 |
282 | if (!value) {
283 | return 'null';
284 | }
285 |
286 | // Make an array to hold the partial results of stringifying this object value.
287 |
288 | gap += indent;
289 | partial = [];
290 |
291 | // Is the value an array?
292 |
293 | if (Object.prototype.toString.apply(value) === '[object Array]') {
294 |
295 | // The value is an array. Stringify every element. Use null as a placeholder
296 | // for non-JSON values.
297 |
298 | length = value.length;
299 | for (i = 0; i < length; i += 1) {
300 | partial[i] = str(i, value) || 'null';
301 | }
302 |
303 | // Join all of the elements together, separated with commas, and wrap them in
304 | // brackets.
305 |
306 | v = partial.length === 0 ? '[]' :
307 | gap ? '[\n' + gap +
308 | partial.join(',\n' + gap) + '\n' +
309 | mind + ']' :
310 | '[' + partial.join(',') + ']';
311 | gap = mind;
312 | return v;
313 | }
314 |
315 | // If the replacer is an array, use it to select the members to be stringified.
316 |
317 | if (rep && typeof rep === 'object') {
318 | length = rep.length;
319 | for (i = 0; i < length; i += 1) {
320 | k = rep[i];
321 | if (typeof k === 'string') {
322 | v = str(k, value);
323 | if (v) {
324 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
325 | }
326 | }
327 | }
328 | } else {
329 |
330 | // Otherwise, iterate through all of the keys in the object.
331 |
332 | for (k in value) {
333 | if (Object.hasOwnProperty.call(value, k)) {
334 | v = str(k, value);
335 | if (v) {
336 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
337 | }
338 | }
339 | }
340 | }
341 |
342 | // Join all of the member texts together, separated with commas,
343 | // and wrap them in braces.
344 |
345 | v = partial.length === 0 ? '{}' :
346 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
347 | mind + '}' : '{' + partial.join(',') + '}';
348 | gap = mind;
349 | return v;
350 | }
351 | }
352 |
353 | // If the JSON object does not yet have a stringify method, give it one.
354 |
355 | if (typeof JSON.stringify !== 'function') {
356 | JSON.stringify = function (value, replacer, space) {
357 |
358 | // The stringify method takes a value and an optional replacer, and an optional
359 | // space parameter, and returns a JSON text. The replacer can be a function
360 | // that can replace values, or an array of strings that will select the keys.
361 | // A default replacer method can be provided. Use of the space parameter can
362 | // produce text that is more easily readable.
363 |
364 | var i;
365 | gap = '';
366 | indent = '';
367 |
368 | // If the space parameter is a number, make an indent string containing that
369 | // many spaces.
370 |
371 | if (typeof space === 'number') {
372 | for (i = 0; i < space; i += 1) {
373 | indent += ' ';
374 | }
375 |
376 | // If the space parameter is a string, it will be used as the indent string.
377 |
378 | } else if (typeof space === 'string') {
379 | indent = space;
380 | }
381 |
382 | // If there is a replacer, it must be a function or an array.
383 | // Otherwise, throw an error.
384 |
385 | rep = replacer;
386 | if (replacer && typeof replacer !== 'function' &&
387 | (typeof replacer !== 'object' ||
388 | typeof replacer.length !== 'number')) {
389 | throw new Error('JSON.stringify');
390 | }
391 |
392 | // Make a fake root object containing our value under the key of ''.
393 | // Return the result of stringifying the value.
394 |
395 | return str('', {'': value});
396 | };
397 | }
398 |
399 |
400 | // If the JSON object does not yet have a parse method, give it one.
401 |
402 | if (typeof JSON.parse !== 'function') {
403 | JSON.parse = function (text, reviver) {
404 |
405 | // The parse method takes a text and an optional reviver function, and returns
406 | // a JavaScript value if the text is a valid JSON text.
407 |
408 | var j;
409 |
410 | function walk(holder, key) {
411 |
412 | // The walk method is used to recursively walk the resulting structure so
413 | // that modifications can be made.
414 |
415 | var k, v, value = holder[key];
416 | if (value && typeof value === 'object') {
417 | for (k in value) {
418 | if (Object.hasOwnProperty.call(value, k)) {
419 | v = walk(value, k);
420 | if (v !== undefined) {
421 | value[k] = v;
422 | } else {
423 | delete value[k];
424 | }
425 | }
426 | }
427 | }
428 | return reviver.call(holder, key, value);
429 | }
430 |
431 |
432 | // Parsing happens in four stages. In the first stage, we replace certain
433 | // Unicode characters with escape sequences. JavaScript handles many characters
434 | // incorrectly, either silently deleting them, or treating them as line endings.
435 |
436 | text = String(text);
437 | cx.lastIndex = 0;
438 | if (cx.test(text)) {
439 | text = text.replace(cx, function (a) {
440 | return '\\u' +
441 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
442 | });
443 | }
444 |
445 | // In the second stage, we run the text against regular expressions that look
446 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
447 | // because they can cause invocation, and '=' because it can cause mutation.
448 | // But just to be safe, we want to reject all unexpected forms.
449 |
450 | // We split the second stage into 4 regexp operations in order to work around
451 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
452 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
453 | // replace all simple value tokens with ']' characters. Third, we delete all
454 | // open brackets that follow a colon or comma or that begin the text. Finally,
455 | // we look to see that the remaining characters are only whitespace or ']' or
456 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
457 |
458 | if (/^[\],:{}\s]*$/.
459 | test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
460 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
461 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
462 |
463 | // In the third stage we use the eval function to compile the text into a
464 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
465 | // in JavaScript: it can begin a block or an object literal. We wrap the text
466 | // in parens to eliminate the ambiguity.
467 |
468 | j = eval('(' + text + ')');
469 |
470 | // In the optional fourth stage, we recursively walk the new structure, passing
471 | // each name/value pair to a reviver function for possible transformation.
472 |
473 | return typeof reviver === 'function' ?
474 | walk({'': j}, '') : j;
475 | }
476 |
477 | // If the text is not JSON parseable, then a SyntaxError is thrown.
478 |
479 | throw new SyntaxError('JSON.parse');
480 | };
481 | }
482 | }());
483 |
--------------------------------------------------------------------------------
/app/javascripts/models.js:
--------------------------------------------------------------------------------
1 | var Project,
2 | Task,
3 | Collection,
4 | Settings;
5 |
6 | Project = new Model('projects');
7 | Task = new Model('tasks');
8 | Settings = new KeyValueModel('settings');
9 | Collection = new KeyValueModel('collections');
10 |
11 | Collection.isActive = function(collectionName) {
12 | return $('#show-' + collectionName).closest('li').hasClass('selected');
13 | };
14 |
15 | Collection.removeItem = function(collectionName, value) {
16 | Collection.set(collectionName, $.map(Collection.get(collectionName), function(innerValue) {
17 | return value === innerValue ? null : innerValue;
18 | }));
19 | };
20 |
21 | Collection.appendItem = function(collectionName, value) {
22 | var items = Collection.get(collectionName);
23 | if (items) {
24 | items.push(value);
25 | Collection.set(collectionName, items);
26 | }
27 | };
28 |
29 | Project.afterCreate = function(project) {
30 | var items = Collection.get('projects') || [];
31 | items.push(project.get('id'));
32 | Collection.set('projects', items);
33 | };
34 |
35 | Project.updateDone = function(project) {
36 | var done = false;
37 | done = Task.findAll({ 'project_id': project.get('id'), 'done': false }).length === 0;
38 | project.set('done', done);
39 | ProjectsController.displayState(project);
40 | };
41 |
42 | Task.updateDueState = function(task) {
43 | var stateIcons = '',
44 | button;
45 | $('#task_' + task.get('id') + ' .task-state-icon').remove();
46 |
47 | if (task.get('due')) {
48 | stateIcons = '';
49 | if (task.get('due') < $.datepicker.formatDate('yy/mm/dd', new Date())) {
50 | stateIcons = '';
51 | }
52 | }
53 |
54 | if (task.get('notes') && task.get('notes').length > 0) {
55 | stateIcons += '';
56 | }
57 |
58 | if (stateIcons.length > 0) {
59 | button = $('#task_' + task.get('id') + ' .ui-button-text')
60 | button.prepend(stateIcons);
61 | }
62 | };
63 |
64 | Task.updateProjectDoneState = function(task) {
65 | if (task.get('project_id')) {
66 | var project = Project.find(task.get('project_id'));
67 | Project.updateDone(project);
68 | }
69 | };
70 |
71 | Task.afterCreate = function(task) {
72 | var items, collection;
73 | Task.updateProjectDoneState(task);
74 |
75 | if (task.get('project_id')) {
76 | collection = 'project_tasks_' + task.get('project_id');
77 | } else if (Collection.isActive('inbox')) {
78 | collection = 'inbox';
79 | } else if (Collection.isActive('today')) {
80 | collection = 'today';
81 | } else if (Collection.isActive('next')) {
82 | collection = 'next';
83 | } else {
84 | return;
85 | }
86 | items = Collection.get(collection) || [];
87 | items.unshift(task.get('id'));
88 | Collection.set(collection, items);
89 | };
90 |
91 | Task.afterUpdate = function(task) {
92 | return Task.updateProjectDoneState(task);
93 | };
94 |
95 | Task.search = function(regex) {
96 | var items = [];
97 |
98 | if (typeof regex === 'string') {
99 | regex = new RegExp(regex, 'i');
100 | }
101 |
102 | jQuery.each(Storage.data[this.collectionName], function(key, item) {
103 | if (item && item.name.match(regex)) {
104 | items.push(new ModelInstance(Task, item));
105 | }
106 | });
107 |
108 | return items;
109 | };
110 |
111 | Collection.inCollection = function(collectionName, task) {
112 | return $.inArray(task.get('id'), Collection.get(collectionName)) != -1;
113 | };
114 |
115 |
--------------------------------------------------------------------------------
/app/javascripts/outro.js:
--------------------------------------------------------------------------------
1 | });
2 |
3 |
--------------------------------------------------------------------------------
/app/javascripts/projects.js:
--------------------------------------------------------------------------------
1 | var ProjectsController = {
2 | displayState: function(project) {
3 | if (project.get('done') === ($('.project-field .state .ui-icon-check').length === 0)) {
4 | TasksController.toggleState($('.project-field .state'));
5 | }
6 | },
7 |
8 | tasks: function(project) {
9 | var tasks = [], items;
10 | items = Collection.get('project_tasks_' + project.get('id')) || [];
11 |
12 | if (items.length === 0) {
13 | tasks = Task.findAll({ 'project_id': project.get('id'), 'archived': false })
14 | } else {
15 | tasks = jQuery.map(items, function(id) {
16 | return Task.find(id);
17 | });
18 |
19 | jQuery.each(tasks, function(index, task) {
20 | if (task.get('archived')) {
21 | delete tasks[index];
22 | }
23 | });
24 | }
25 |
26 | return tasks;
27 | },
28 |
29 | display: function(project, element) {
30 | if (!element) {
31 | element = $('#show_project_' + project.get('id'));
32 | }
33 |
34 | var name = (project.get('name') || '').length === 0 ? defaultFieldValues.project_name : project.get('name')
35 | $('.project-field').show();
36 | $('.project-header .name-text').html(name);
37 |
38 | /*
39 | if (project.get('tags')) {
40 | $('.project-header .tags').html(project.get('tags'));
41 | } else {
42 | $('.project-header .tags').html('Tags');
43 | }
44 | */
45 |
46 | if (project.get('notes')) {
47 | $('.project-header .notes').html(project.get('notes'));
48 | } else {
49 | $('.project-header .notes').html(defaultFieldValues.project_notes);
50 | }
51 |
52 | var tasks = ProjectsController.tasks(project);
53 | TasksController.display(tasks);
54 | ProjectsController.displayState(project);
55 |
56 | if (project.get('due')) {
57 | $('.project-header .due-date').html(presentDate(project.get('due')));
58 | } else {
59 | $('.project-header .due-date').html('Due Date');
60 | }
61 |
62 | $('#project-info').show();
63 |
64 | if (Settings.get('outline-view-width')) {
65 | $('.outline-view').css({ width: Settings.get('outline-view-width') });
66 | }
67 | },
68 |
69 | displayAll: function() {
70 | $('.outline-view .projects li').remove();
71 |
72 | jQuery.each(Collection.get('projects') || [], function(index, value) {
73 | ProjectsController.insert(Project.find(value));
74 | });
75 | selectProject();
76 | },
77 |
78 | insert: function(project) {
79 | if (!project) return;
80 | var name = (project.get('name') || '').length === 0 ? defaultFieldValues.project_name : project.get('name'),
81 | html = $('Select a new location for this task:
' + text + '
'); 307 | }; 308 | 309 | this.pass = function(message) { 310 | display('' + message + '
'); 311 | }; 312 | 313 | this.fail = function(message) { 314 | display('' + message + '
'); 315 | }; 316 | 317 | this.error = function(message, exception) { 318 | this.fail(message); 319 | display('Exception: ' + exception + '
'); 320 | }; 321 | 322 | this.context = function(name) { 323 | display('Enter an OpenID to continue.
4 | 11 |
12 | <%= text_field_tag 'openid_url', cookies[:open_id], :size => 30 %>
13 |
15 | Login OpenID Help 16 |
17 | <% end %> 18 | 19 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Wingman::Application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'action_controller/railtie' 4 | require 'action_mailer/railtie' 5 | require 'active_resource/railtie' 6 | 7 | # If you have a Gemfile, require the gems listed there, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(:default, Rails.env) if defined?(Bundler) 10 | 11 | module Wingman 12 | class Application < Rails::Application 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration should go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded. 16 | 17 | # Custom directories with classes and modules you want to be autoloadable. 18 | # config.autoload_paths += %W(#{config.root}/extras) 19 | 20 | # Only load the plugins named here, in the order given (default is alphabetical). 21 | # :all can be used as a placeholder for all plugins not explicitly named. 22 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 23 | 24 | # Activate observers that should always be running. 25 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 26 | 27 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 28 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 29 | # config.time_zone = 'Central Time (US & Canada)' 30 | 31 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 32 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 33 | # config.i18n.default_locale = :de 34 | 35 | # JavaScript files you want as :defaults (application.js is always included). 36 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 37 | 38 | # Configure the default encoding used in templates for Ruby 1.9. 39 | config.encoding = "utf-8" 40 | 41 | # Configure sensitive parameters which will be filtered from the log file. 42 | config.filter_parameters += [:password] 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Wingman::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Wingman::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | end 23 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Wingman::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = true 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Wingman::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | end 36 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/initializers/openid.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/wingman/3e448c682631877811184fb5f62af94107800eb9/config/initializers/openid.rb -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | # Wingman::Application.config.secret_token = '43d25a9249e916d58373586193a2bbc2a0940c7623d2485255c82e7bfc7fddd037e5a643c4550ba5079fe03d040bd5414a0f955483f6adc9f945ecf2c8fe6b83' 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Wingman::Application.config.session_store :cookie_store, :key => '_wingman_session' 4 | Wingman::Application.config.session_store :mongoid_store, :key => '_wingman_session' 5 | 6 | # Use the database for sessions instead of the cookie-based default, 7 | # which shouldn't be used to store highly confidential information 8 | # (create the session table with "rake db:sessions:create") 9 | # Wingman::Application.config.session_store :active_record_store 10 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /config/mongoid.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | 3 | development: 4 | <<: *defaults 5 | host: localhost 6 | port: 27017 7 | database: wingman_development 8 | 9 | test: 10 | <<: *defaults 11 | database: wingman_test 12 | host: localhost 13 | port: 27017 14 | 15 | # set these environment variables on your production server 16 | production: 17 | <<: *defaults 18 | host: <%= ENV['MONGOID_HOST'] %> 19 | port: <%= ENV['MONGOID_PORT'] %> 20 | database: <%= ENV['MONGOID_DATABASE'] %> 21 | username: <%= ENV['MONGOID_USERNAME'] %> 22 | password: <%= ENV['MONGOID_PASSWORD'] %> 23 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Wingman::Application.routes.draw do 2 | root :to => '', :action => 'main', :controller => 'application' 3 | match 'logout' => 'application#logout' 4 | match 'javascripts/all-development.js' => 'application#alljs' 5 | 6 | resource :storage, :controller => 'storage', :as => :storage do 7 | member do 8 | put :set_key_value 9 | get :restore 10 | get :archive 11 | post :update_user 12 | end 13 | end 14 | 15 | resource :openid, :controller => 'openid', :as => :openid do 16 | member do 17 | get :complete 18 | get :index 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /lib/db_store.rb: -------------------------------------------------------------------------------- 1 | require 'openid/store/interface' 2 | 3 | module OpenID::Store 4 | class Association 5 | include Mongoid::Document 6 | field :secret, :type => Binary 7 | 8 | def from_record 9 | OpenID::Association.new(handle, secret.to_s, issued, lifetime, assoc_type) 10 | end 11 | end 12 | 13 | class Nonce 14 | include Mongoid::Document 15 | end 16 | 17 | class DbStore < OpenID::Store::Interface 18 | def self.cleanup_nonces 19 | now = Time.now.to_i 20 | Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew]) 21 | end 22 | 23 | def self.cleanup_associations 24 | now = Time.now.to_i 25 | Association.delete_all(['issued + lifetime > ?',now]) 26 | end 27 | 28 | def store_association(server_url, assoc) 29 | remove_association(server_url, assoc.handle) 30 | 31 | # BSON::Binary is used because secrets raise an exception 32 | # due to character encoding 33 | Association.create(:server_url => server_url, 34 | :handle => assoc.handle, 35 | :secret => BSON::Binary.new(assoc.secret), 36 | :issued => assoc.issued, 37 | :lifetime => assoc.lifetime, 38 | :assoc_type => assoc.assoc_type) 39 | end 40 | 41 | def get_association(server_url, handle = nil) 42 | assocs = if handle.blank? 43 | Association.find :all, :conditions => { :server_url => server_url } 44 | else 45 | Association.find :all, :conditions => { :server_url => server_url, :handle => handle } 46 | end 47 | 48 | assocs.reverse.each do |assoc| 49 | a = assoc.from_record 50 | if a.expires_in == 0 51 | assoc.destroy 52 | else 53 | return a 54 | end 55 | end if assocs.any? 56 | 57 | return nil 58 | end 59 | 60 | def remove_association(server_url, handle) 61 | Association.find(:all, :conditions => { :server_url => server_url, :handle => handle }).each do |assoc| 62 | assoc.destroy! 63 | end 64 | end 65 | 66 | def use_nonce(server_url, timestamp, salt) 67 | return false if Nonce.find(:first, :conditions => { :server_url => server_url, :timestamp => timestamp, :salt => salt}) 68 | return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew 69 | Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt) 70 | return true 71 | end 72 | end 73 | end 74 | 75 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/wingman/3e448c682631877811184fb5f62af94107800eb9/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/js.rake: -------------------------------------------------------------------------------- 1 | require File.join(Rails.root, 'app', 'models', 'wingman') 2 | 3 | namespace :js do 4 | task :build do 5 | File.open(File.join(Rails.root, 'public', 'javascripts', 'all.js'), 'w+') do |f| 6 | f << Wingman.alljs 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |You may have mistyped the address or the page may have moved.
24 |Maybe you tried to change something you didn't have access to.
24 |We've been notified about this issue and we'll take a look at it shortly.
24 |