diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 00000000..e17fde3f --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,11 @@ +class UsersController < ApplicationController + + # Currently used to display users nick and ids for autocomplete + def index + @users = User.where("nick LIKE ?", "%#{params[:q]}%") + respond_to do |format| + format.json { render :json => @users.map { |u| {:id => u.id, :name => u.nick} } } + end + end + +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 00000000..2310a240 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/models/task.rb b/app/models/task.rb index 62a95699..5857a37a 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -27,13 +27,13 @@ class Task < ActiveRecord::Base def enough_users_assigned? assignments.find_all_by_accepted(true).size >= required_users ? true : false end - - # extracts nicknames from a comma seperated string + + # Get users from comma seperated ids # and makes the users responsible for the task - def user_list=(string) - @user_list = string.split(%r{,\s*}) - new_users = @user_list - users.collect(&:nick) - old_users = users.reject { |user| @user_list.include?(user.nick) } + def user_list=(ids) + list = ids.split(",") + new_users = list - users.collect(&:id) + old_users = users.reject { |user| list.include?(user.id) } logger.debug "New users: #{new_users}" logger.debug "Old users: #{old_users}" @@ -44,8 +44,8 @@ class Task < ActiveRecord::Base assignments.find(:all, :conditions => ["user_id IN (?)", old_users.collect(&:id)]).each(&:destroy) end # create new assignments - new_users.each do |nick| - user = User.find_by_nick(nick) + new_users.each do |id| + user = User.find(id) if user.blank? errors.add(:user_list) else @@ -62,7 +62,7 @@ class Task < ActiveRecord::Base end def user_list - @user_list ||= users.collect(&:nick).join(", ") + @user_list ||= users.collect(&:id).join(", ") end private diff --git a/app/views/layouts/application.haml b/app/views/layouts/application.haml index e64710e2..bfd1c566 100644 --- a/app/views/layouts/application.haml +++ b/app/views/layouts/application.haml @@ -3,12 +3,12 @@ %head %meta{"http-equiv" => "content-type", :content => "text/html;charset=UTF-8"} %title= "FoodSoft - " + (yield(:title) or controller.controller_name) - = stylesheet_link_tag 'main', 'rails_messages', 'nav', 'simple_form', :cache => "all_cached" + = stylesheet_link_tag 'main', 'rails_messages', 'nav', 'simple_form', 'token-input', :cache => "all_cached" = stylesheet_link_tag "print", :media => "print" - = javascript_include_tag 'jquery.min', 'jquery-ui.min', 'jquery_ujs', 'application', 'ordering', :cache => "all_cached" + = javascript_include_tag 'jquery.min', 'jquery-ui.min', 'jquery_ujs', 'jquery.tokeninput', 'application', 'ordering', :cache => "all_cached" = yield(:head) %body #logininfo= render :partial => 'shared/loginInfo' diff --git a/app/views/tasks/_form.html.haml b/app/views/tasks/_form.html.haml index ecdcc0ab..032178d9 100644 --- a/app/views/tasks/_form.html.haml +++ b/app/views/tasks/_form.html.haml @@ -1,8 +1,17 @@ +- content_for :head do + :javascript + $(function() { + $("#task_user_list").tokenInput("#{users_path(:format => :json)}", { + crossDomain: false, + prePopulate: $("#task_user_list").data("pre") + }); + }); + = simple_form_for @task do |f| = f.input :name = f.input :description = f.input :duration, :as => :select, :collection => 1..3 - = f.input :user_list, :as => :string + = f.input :user_list, :as => :string, :input_html => { 'data-pre' => @task.users.map { |u| {:id => u.id, :name => u.nick} }.to_json } = f.input :required_users = f.association :workgroup = f.input :due_date, :include_blank => true diff --git a/config/routes.rb b/config/routes.rb index 7c55ceb1..3994aaaa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,7 +23,6 @@ Foodsoft::Application.routes.draw do match '/home/profile' => 'home#profile', :as => 'my_profile' match '/home/ordergroup' => 'home#ordergroup', :as => 'my_ordergroup' - ############ Wiki resources :pages do @@ -157,6 +156,8 @@ Foodsoft::Application.routes.draw do ############## The rest + resources :users, :only => [:index] + match '/:controller(/:action(/:id))' end # End of /:foodcoop scope diff --git a/public/javascripts/jquery.tokeninput.js b/public/javascripts/jquery.tokeninput.js new file mode 100644 index 00000000..4618d67a --- /dev/null +++ b/public/javascripts/jquery.tokeninput.js @@ -0,0 +1,718 @@ +/* + * jQuery Plugin: Tokenizing Autocomplete Text Entry + * Version 1.4.2 + * + * Copyright (c) 2009 James Smith (http://loopj.com) + * Licensed jointly under the GPL and MIT licenses, + * choose which one suits your project best! + * + */ + +(function ($) { +// Default settings +var DEFAULT_SETTINGS = { + hintText: "Type in a search term", + noResultsText: "No results", + searchingText: "Searching...", + deleteText: "×", + searchDelay: 300, + minChars: 1, + tokenLimit: null, + jsonContainer: null, + method: "GET", + contentType: "json", + queryParam: "q", + tokenDelimiter: ",", + preventDuplicates: false, + prePopulate: null, + animateDropdown: true, + onResult: null, + onAdd: null, + onDelete: null +}; + +// Default classes to use when theming +var DEFAULT_CLASSES = { + tokenList: "token-input-list", + token: "token-input-token", + tokenDelete: "token-input-delete-token", + selectedToken: "token-input-selected-token", + highlightedToken: "token-input-highlighted-token", + dropdown: "token-input-dropdown", + dropdownItem: "token-input-dropdown-item", + dropdownItem2: "token-input-dropdown-item2", + selectedDropdownItem: "token-input-selected-dropdown-item", + inputToken: "token-input-input-token" +}; + +// Input box position "enum" +var POSITION = { + BEFORE: 0, + AFTER: 1, + END: 2 +}; + +// Keys "enum" +var KEY = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + NUMPAD_ENTER: 108, + COMMA: 188 +}; + + +// Expose the .tokenInput function to jQuery as a plugin +$.fn.tokenInput = function (url_or_data, options) { + var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); + + return this.each(function () { + new $.TokenList(this, url_or_data, settings); + }); +}; + + +// TokenList class for each input +$.TokenList = function (input, url_or_data, settings) { + // + // Initialization + // + + // Configure the data source + if($.type(url_or_data) === "string") { + // Set the url to query against + settings.url = url_or_data; + + // Make a smart guess about cross-domain if it wasn't explicitly specified + if(settings.crossDomain === undefined) { + if(settings.url.indexOf("://") === -1) { + settings.crossDomain = false; + } else { + settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]); + } + } + } else if($.type(url_or_data) === "array") { + // Set the local data to search through + settings.local_data = url_or_data; + } + + // Build class names + if(settings.classes) { + // Use custom class names + settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); + } else if(settings.theme) { + // Use theme-suffixed default class names + settings.classes = {}; + $.each(DEFAULT_CLASSES, function(key, value) { + settings.classes[key] = value + "-" + settings.theme; + }); + } else { + settings.classes = DEFAULT_CLASSES; + } + + + // Save the tokens + var saved_tokens = []; + + // Keep track of the number of tokens in the list + var token_count = 0; + + // Basic cache to save on db hits + var cache = new $.TokenList.Cache(); + + // Keep track of the timeout, old vals + var timeout; + var input_val; + + // Create a new text input an attach keyup events + var input_box = $("") + .css({ + outline: "none" + }) + .focus(function () { + if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { + show_dropdown_hint(); + } + }) + .blur(function () { + hide_dropdown(); + }) + .bind("keyup keydown blur update", resize_input) + .keydown(function (event) { + var previous_token; + var next_token; + + switch(event.keyCode) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + if(!$(this).val()) { + previous_token = input_token.prev(); + next_token = input_token.next(); + + if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { + // Check if there is a previous/next token and it is selected + if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { + deselect_token($(selected_token), POSITION.BEFORE); + } else { + deselect_token($(selected_token), POSITION.AFTER); + } + } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { + // We are moving left, select the previous token if it exists + select_token($(previous_token.get(0))); + } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { + // We are moving right, select the next token if it exists + select_token($(next_token.get(0))); + } + } else { + var dropdown_item = null; + + if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { + dropdown_item = $(selected_dropdown_item).next(); + } else { + dropdown_item = $(selected_dropdown_item).prev(); + } + + if(dropdown_item.length) { + select_dropdown_item(dropdown_item); + } + return false; + } + break; + + case KEY.BACKSPACE: + previous_token = input_token.prev(); + + if(!$(this).val().length) { + if(selected_token) { + delete_token($(selected_token)); + } else if(previous_token.length) { + select_token($(previous_token.get(0))); + } + + return false; + } else if($(this).val().length === 1) { + hide_dropdown(); + } else { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search();}, 5); + } + break; + + case KEY.TAB: + case KEY.ENTER: + case KEY.NUMPAD_ENTER: + case KEY.COMMA: + if(selected_dropdown_item) { + add_token($(selected_dropdown_item)); + return false; + } + break; + + case KEY.ESCAPE: + hide_dropdown(); + return true; + + default: + if(String.fromCharCode(event.which)) { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search();}, 5); + } + break; + } + }); + + // Keep a reference to the original input box + var hidden_input = $(input) + .hide() + .val("") + .focus(function () { + input_box.focus(); + }) + .blur(function () { + input_box.blur(); + }); + + // Keep a reference to the selected token and dropdown item + var selected_token = null; + var selected_dropdown_item = null; + + // The list to store the token items in + var token_list = $("