├── init.rb ├── Rakefile ├── lib ├── feedback │ └── version.rb ├── feedback.rb └── generators │ └── feedback_form │ ├── templates │ ├── images │ │ ├── loading.gif │ │ ├── closelabel.gif │ │ ├── feedback_tab.png │ │ └── feedback_tab_h.png │ ├── feedback_mailer_test.rb.erb │ ├── feedback_mailer.rb.erb │ ├── views │ │ ├── feedback_mailer │ │ │ └── feedback.html.erb │ │ └── feedbacks │ │ │ └── new.html.erb │ ├── feedback_model.rb.erb │ ├── feedback_test.rb.erb │ ├── feedbacks_helper.rb.jquery.erb │ ├── feedbacks_helper.rb.prototype.erb │ ├── feedbacks_controller.rb.erb │ ├── feedbacks_controller_test.rb.erb │ ├── feedback.css.scss │ ├── jquery.feedback.js.coffee │ └── prototype.feedback.js.coffee │ ├── USAGE │ └── feedback_form_generator.rb ├── Gemfile ├── generators └── feedback_form │ ├── templates │ ├── images │ │ ├── loading.gif │ │ ├── closelabel.gif │ │ ├── feedback_tab.png │ │ └── feedback_tab_h.png │ ├── feedback_mailer_test.rb.erb │ ├── views │ │ ├── feedback_mailer │ │ │ └── feedback.html.erb │ │ └── feedbacks │ │ │ └── new.html.erb │ ├── feedback_mailer.rb.erb │ ├── feedback_model.rb.erb │ ├── feedback_test.rb.erb │ ├── feedbacks_helper.rb.prototype.erb │ ├── feedbacks_helper.rb.jquery.erb │ ├── feedbacks_controller.rb.erb │ ├── feedbacks_controller_test.rb.erb │ ├── feedback.css │ ├── jquery.feedback.js │ └── prototype.feedback.js │ ├── USAGE │ ├── lib │ └── insert_routes.rb │ └── feedback_form_generator.rb ├── .gitignore ├── feedback.gemspec ├── MIT-LICENSE └── README.md /init.rb: -------------------------------------------------------------------------------- 1 | # Include hook code here 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/feedback/version.rb: -------------------------------------------------------------------------------- 1 | module Feedback 2 | VERSION = "0.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /lib/feedback.rb: -------------------------------------------------------------------------------- 1 | require "feedback/version" 2 | 3 | module Feedback 4 | # Your code goes here... 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in feedback.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/generators/feedback_form/templates/images/loading.gif -------------------------------------------------------------------------------- /generators/feedback_form/templates/images/closelabel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/generators/feedback_form/templates/images/closelabel.gif -------------------------------------------------------------------------------- /generators/feedback_form/templates/images/feedback_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/generators/feedback_form/templates/images/feedback_tab.png -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/lib/generators/feedback_form/templates/images/loading.gif -------------------------------------------------------------------------------- /generators/feedback_form/templates/images/feedback_tab_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/generators/feedback_form/templates/images/feedback_tab_h.png -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/images/closelabel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/lib/generators/feedback_form/templates/images/closelabel.gif -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/images/feedback_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/lib/generators/feedback_form/templates/images/feedback_tab.png -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/images/feedback_tab_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsboulanger/feedback/HEAD/lib/generators/feedback_form/templates/images/feedback_tab_h.png -------------------------------------------------------------------------------- /generators/feedback_form/USAGE: -------------------------------------------------------------------------------- 1 | ./script/generate feedback_form [NAME] 2 | 3 | The [NAME] is optional and will be used to 4 | name all classes and files, the default is: feedback 5 | It should be singular. -------------------------------------------------------------------------------- /lib/generators/feedback_form/USAGE: -------------------------------------------------------------------------------- 1 | rails generate feedback_form [NAME] 2 | 3 | The [NAME] is optional and will be used to 4 | name all classes and files, the default is: feedback 5 | It should be singular. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedback_mailer_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | 4 | class <%= mailer_class_name %>Test < ActionMailer::TestCase 5 | # replace this with your real tests 6 | test "the truth" do 7 | assert true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedback_mailer_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | 4 | class <%= mailer_class_name %>Test < ActionMailer::TestCase 5 | # replace this with your real tests 6 | test "the truth" do 7 | assert true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedback_mailer.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= mailer_class_name %> < ActionMailer::Base 2 | default :from => 'noreply@yoursite.com' 3 | 4 | def feedback(feedback) 5 | @feedback = feedback 6 | mail(:to => 'webmaster@yoursite.com', :subject => '[Feedback for YourSite] #{feedback.subject}') 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/views/feedback_mailer/feedback.html.erb: -------------------------------------------------------------------------------- 1 | Dear site administrator, 2 | 3 | You're lucky today, you've got feedback from your users: 4 | 5 | Subject: <%=h @feedback.subject %> 6 | User's email: <%=h @feedback.email %> 7 | Sent at: <%= Time.now %> 8 | From Page: <%= @feedback.page %> 9 | 10 | Comment: 11 | <%=h @feedback.comment %> 12 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/views/feedback_mailer/feedback.html.erb: -------------------------------------------------------------------------------- 1 | Dear site administrator, 2 | 3 | You're lucky today, you've got feedback from your users: 4 | 5 | Subject: <%=h @feedback.subject %> 6 | User's email: <%=h @feedback.email %> 7 | Sent at: <%= Time.now %> 8 | From Page: <%= @feedback.page %> 9 | 10 | Comment: 11 | <%=h @feedback.comment %> 12 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedback_mailer.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= mailer_class_name %> < ActionMailer::Base 2 | 3 | def feedback(feedback) 4 | @recipients = 'webmaster@yoursite.com' 5 | @from = 'noreply@yoursite.com' 6 | @subject = "[Feedback for YourSite] #{feedback.subject}" 7 | @sent_on = Time.now 8 | @body[:feedback] = feedback 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedback_model.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= model_class_name %> 2 | attr_accessor :subject, :email, :comment, :page 3 | 4 | def initialize(params = {}) 5 | self.subject = params[:subject] 6 | self.email = params[:email] 7 | self.comment = params[:comment] 8 | self.page = params[:page] 9 | end 10 | 11 | def valid? 12 | self.comment && !self.comment.strip.blank? 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedback_model.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= model_class_name %> 2 | attr_accessor :subject, :email, :comment, :page 3 | 4 | def initialize(params = {}) 5 | self.subject = params[:subject] 6 | self.email = params[:email] 7 | self.comment = params[:comment] 8 | self.page = params[:page] 9 | end 10 | 11 | def valid? 12 | self.comment && !self.comment.strip.blank? 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedback_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class <%= model_class_name %>Test < Test::Unit::TestCase 4 | def test_should_require_comment 5 | assert !create_comment(:comment => " ").valid? 6 | end 7 | 8 | def test_should_be_valid 9 | assert create_comment.valid? 10 | end 11 | 12 | protected 13 | def create_comment(params = {}) 14 | valid_feedback = { 15 | :subject => "Test", 16 | :email => "test@yoursite.com", 17 | :comment => "i like the site" 18 | } 19 | <%= model_class_name %>.new(valid_feedback.merge(params)) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedback_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class <%= model_class_name %>Test < Test::Unit::TestCase 4 | 5 | 6 | def test_should_require_comment 7 | assert !create_comment(:comment => " ").valid? 8 | end 9 | 10 | def test_should_be_valid 11 | assert create_comment.valid? 12 | end 13 | 14 | protected 15 | 16 | def create_comment(params = {}) 17 | valid_feedback = { 18 | :subject => "Test", 19 | :email => "test@yoursite.com", 20 | :comment => "i like the site" 21 | } 22 | <%= model_class_name %>.new(valid_feedback.merge(params)) 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /generators/feedback_form/templates/views/feedbacks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Feedback

2 |

Please leave us feedback, it's really appreciated.

3 | <% form_for :feedback, @feedback, :url => feedback_path, :html => {:id => "feedback_form"} do |f| -%> 4 | <%= f.hidden_field 'page' %> 5 | <% unless @error_message.blank? %> 6 |

7 | <%=h @error_message %> 8 |

9 | <% end %> 10 |

11 | <%= f.label 'subject' %> 12 | <%= f.select 'subject', ['Problem', 'Suggestion', 'Question', 'Other'] %> 13 |

14 |

15 | <%= f.label 'email' %> 16 | <%= f.text_field 'email' %> 17 |

18 |

19 | <%= f.label 'comment' %>
20 | <%= f.text_area 'comment', :rows => 10, :cols => 30 %> 21 |

22 |

<%= f.submit 'Send' %>

23 | 24 | <% end -%> 25 | 26 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedbacks_helper.rb.jquery.erb: -------------------------------------------------------------------------------- 1 | module <%= helper_class_name %> 2 | 3 | def feedback_tab(options = {}) 4 | feedback_init({'position' => 'top'}.merge(options.stringify_keys)) 5 | end 6 | 7 | def feedback_init(options = {}) 8 | options = { 9 | "position" => "null" 10 | }.merge(options.stringify_keys) 11 | 12 | options['position'] = "'#{options['position']}'" unless options['position'].blank? || options['position'] == 'null' 13 | content_tag 'script', :type => "text/javascript" do 14 | "$(document).ready(function() { $('.feedback_link').feedback({tabPosition: #{options["position"]}}); });".html_safe 15 | end 16 | end 17 | 18 | def feedback_link(text, options = {}) 19 | link_to text, '#', :class => "feedback_link" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedbacks_helper.rb.prototype.erb: -------------------------------------------------------------------------------- 1 | module <%= helper_class_name %> 2 | 3 | def feedback_tab(options = {}) 4 | feedback_init({'position' => 'top'}.merge(options.stringify_keys)) 5 | end 6 | 7 | def feedback_init(options = {}) 8 | options = { 9 | "position" => "null" 10 | }.merge(options.stringify_keys) 11 | 12 | options['position'] = "'#{options['position']}'" unless options['position'].blank? || options['position'] == 'null' 13 | content_tag 'script', :type => "text/javascript" do 14 | "document.observe(\"dom:loaded\", function() { Feedback.init({tabPosition: #{options["position"]}}); });".html_safe 15 | end 16 | end 17 | 18 | def feedback_link(text, options = {}) 19 | link_to text, '#', :class => "feedback_link" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/views/feedbacks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Feedback

2 | 3 |

Please leave us feedback, it's really appreciated.

4 | 5 | <%= form_for @feedback, :as => :feedback, :url => feedback_path, :html => {:id => "feedback_form"} do |f| %> 6 | <%= f.hidden_field 'page' %> 7 | <% unless @error_message.blank? %> 8 |

9 | <%=h @error_message %> 10 |

11 | <% end %> 12 | 13 |

14 | <%= f.label 'subject' %> 15 | <%= f.select 'subject', ['Problem', 'Suggestion', 'Question', 'Other'] %> 16 |

17 |

18 | <%= f.label 'email' %> 19 | <%= f.text_field 'email' %> 20 |

21 |

22 | <%= f.label 'comment' %>
23 | <%= f.text_area 'comment', :rows => 10, :cols => 30 %> 24 |

25 |

<%= f.submit 'Send' %>

26 | <% end %> 27 | -------------------------------------------------------------------------------- /feedback.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'feedback/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "feedback" 8 | gem.version = Feedback::VERSION 9 | gem.authors = ["Peter Wong"] 10 | gem.email = ["dohkoos@gmail.com"] 11 | gem.description = %q{feedback plugin for web frameworks} 12 | gem.summary = %q{feedback provides your app an ajax-based feedback form triggered by a sticky tab.} 13 | gem.homepage = "https://github.com/dohkoos/feedback" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedbacks_controller.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= controller_class_name %> < ApplicationController 2 | layout false 3 | 4 | def new 5 | @feedback = <%= model_class_name %>.new(page: request.referer) 6 | end 7 | 8 | def create 9 | @feedback = <%= model_class_name %>.new(params[:feedback]) 10 | if @feedback.valid? 11 | <%= mailer_class_name %>.feedback(@feedback).deliver 12 | render :status => :created, :text => '

Thank you for your feedback!

' 13 | else 14 | @error_message = "Please enter your #{@feedback.subject.to_s.downcase}" 15 | 16 | # Returns the whole form back. This is not the most effective 17 | # use of AJAX as we could return the error message in JSON, but 18 | # it makes easier the customization of the form with error messages 19 | # without worrying about the javascript. 20 | render :action => 'new', :status => :unprocessable_entity 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedbacks_helper.rb.prototype.erb: -------------------------------------------------------------------------------- 1 | module <%= helper_class_name %> 2 | 3 | def feedback_tab(options = {}) 4 | feedback_init({'position' => 'top'}.merge(options.stringify_keys)) 5 | end 6 | 7 | def feedback_init(options = {}) 8 | options = { 9 | "position" => "null" 10 | }.merge(options.stringify_keys) 11 | 12 | options['position'] = "'#{options['position']}'" unless options['position'].blank? || options['position'] == 'null' 13 | content_tag 'script', :type => "text/javascript" do 14 | "document.observe(\"dom:loaded\", function() { Feedback.init({tabPosition: #{options["position"]}}); });" 15 | end 16 | 17 | end 18 | 19 | def feedback_includes() 20 | stylesheet_link_tag('feedback') + 21 | javascript_include_tag('prototype.feedback.js') 22 | end 23 | 24 | def feedback_link(text, options = {}) 25 | link_to text, '#', :class => "feedback_link" 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedbacks_helper.rb.jquery.erb: -------------------------------------------------------------------------------- 1 | module <%= helper_class_name %> 2 | 3 | 4 | 5 | def feedback_init(options = {}) 6 | options = { 7 | "position" => "null" 8 | }.merge(options.stringify_keys) 9 | 10 | options['position'] = "'#{options['position']}'" unless options['position'].blank? || options['position'] == 'null' 11 | content_tag 'script', :type => "text/javascript" do 12 | "$(document).ready(function() { $('.feedback_link').feedback({tabPosition: #{options["position"]}}); });" 13 | end 14 | 15 | end 16 | 17 | def feedback_includes() 18 | stylesheet_link_tag('feedback') + 19 | javascript_include_tag('jquery.feedback.js') 20 | end 21 | 22 | def feedback_tab(options = {}) 23 | feedback_init({'position' => 'top'}.merge(options.stringify_keys)) 24 | end 25 | 26 | 27 | def feedback_link(text, options = {}) 28 | link_to text, '#', :class => "feedback_link" 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedbacks_controller.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= controller_class_name %> < ApplicationController 2 | layout false 3 | 4 | def new 5 | @feedback = <%= model_class_name %>.new(page: request.referer) 6 | end 7 | 8 | def create 9 | 10 | @feedback = <%= model_class_name %>.new(params[:feedback]) 11 | if @feedback.valid? 12 | <%= mailer_class_name %>.deliver_feedback(@feedback) 13 | render :status => :created, :text => '

Thank you for your feedback!

' 14 | else 15 | @error_message = "Please enter your #{@feedback.subject.to_s.downcase}" 16 | 17 | # Returns the whole form back. This is not the most effective 18 | # use of AJAX as we could return the error message in JSON, but 19 | # it makes easier the customization of the form with error messages 20 | # without worrying about the javascript. 21 | render :action => 'new', :status => :unprocessable_entity 22 | end 23 | 24 | 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Jean-Sebastien Boulanger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedbacks_controller_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class <%= controller_class_name %>Test < ActionController::TestCase 4 | def setup 5 | @controller = <%= controller_class_name %>.new 6 | @request = ActionController::TestRequest.new 7 | @response = ActionController::TestResponse.new 8 | end 9 | 10 | def test_should_have_minimal_feedback_form 11 | get :new 12 | assert_select "form#feedback_form", true do 13 | assert_select "[action=?]", "/feedbacks" 14 | assert_select "[method=?]", /post/i 15 | assert_select "textarea[name=?]", "feedback[comment]" 16 | end 17 | end 18 | 19 | def test_should_post_create 20 | post :create, :feedback => {:comment => "Great website!"} 21 | assert :success # Doesn't test much 22 | assert_nil @error_message 23 | end 24 | 25 | def test_should_set_error_message_when_not_valid 26 | post :create, :feedback => {:comment => ""} 27 | assert !assigns(:error_message).blank? 28 | end 29 | 30 | protected 31 | def create_feedback(params = {}) 32 | valid_feedback = { 33 | :subject => "Test", 34 | :email => "test@yoursite.com", 35 | :comment => "i like the site" 36 | } 37 | <%= model_class_name %>.new(valid_feedback.merge(params)) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /generators/feedback_form/lib/insert_routes.rb: -------------------------------------------------------------------------------- 1 | 2 | # Adapted from restful_authentication plugin 3 | Rails::Generator::Commands::Create.class_eval do 4 | def route_name(name, path, route_options = {}) 5 | sentinel = 'ActionController::Routing::Routes.draw do |map|' 6 | 7 | logger.route "map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 8 | gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match| 9 | "#{match}\n map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 10 | end 11 | end 12 | end 13 | 14 | Rails::Generator::Commands::Destroy.class_eval do 15 | 16 | def route_name(name, path, route_options = {}) 17 | look_for = "\n map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 18 | logger.route "map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 19 | gsub_file 'config/routes.rb', /(#{look_for})/mi, '' 20 | end 21 | end 22 | 23 | Rails::Generator::Commands::List.class_eval do 24 | 25 | def route_name(name, path, options = {}) 26 | logger.route "map.#{name} '#{path}', :controller => '{options[:controller]}', :action => '#{options[:action]}'" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedbacks_controller_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class <%= controller_class_name %>Test < ActionController::TestCase 4 | 5 | def setup 6 | @controller = <%= controller_class_name %>.new 7 | @request = ActionController::TestRequest.new 8 | @response = ActionController::TestResponse.new 9 | end 10 | 11 | 12 | def test_should_have_minimal_feedback_form 13 | get :new 14 | assert_select "form#feedback_form", true do 15 | assert_select "[action=?]", "/feedbacks" 16 | assert_select "[method=?]", /post/i 17 | assert_select "textarea[name=?]", "feedback[comment]" 18 | end 19 | end 20 | 21 | def test_should_post_create 22 | post :create, :feedback => {:comment => "Great website!"} 23 | assert :success # Doesn't test much 24 | assert_nil @error_message 25 | end 26 | 27 | 28 | def test_should_set_error_message_when_not_valid 29 | post :create, :feedback => {:comment => ""} 30 | assert !assigns(:error_message).blank? 31 | end 32 | 33 | 34 | protected 35 | 36 | def create_feedback(params = {}) 37 | valid_feedback = { 38 | :subject => "Test", 39 | :email => "test@yoursite.com", 40 | :comment => "i like the site" 41 | } 42 | <%= model_class_name %>.new(valid_feedback.merge(params)) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/feedback.css.scss: -------------------------------------------------------------------------------- 1 | div#feedback { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | z-index: 100001; } 8 | 9 | a { 10 | &#feedback_link { 11 | position: fixed; 12 | z-index: 99999; 13 | background-color: #CC0000; 14 | width: 26px; 15 | height: 96px; 16 | overflow: hidden; 17 | background: #c00 image-url('feedback/feedback_tab.png'); 18 | _position: absolute; 19 | /*_background-image: image-url('feedback/feedback_tab.png');*/ 20 | &:hover { 21 | background-color: #999; } 22 | &.left { 23 | left: 0; 24 | top: 25%; 25 | width: 26px; 26 | height: 96px; 27 | background-image: image-url('feedback/feedback_tab.png'); 28 | _background-image: image-url('feedback/feedback_tab.png'); } 29 | &.right { 30 | right: 0; 31 | top: 25%; 32 | width: 26px; 33 | height: 96px; 34 | background-image: image-url('feedback/feedback_tab.png'); 35 | _background-image: image-url('feedback/feedback_tab.png'); } 36 | &.top { 37 | right: 10%; 38 | top: 0; 39 | width: 96px; 40 | height: 26px; 41 | background-image: image-url('feedback/feedback_tab_h.png'); 42 | _background-image: image-url('feedback/feedback_tab_h.png'); } 43 | &.bottom { 44 | right: 10%; 45 | bottom: 0; 46 | width: 96px; 47 | height: 26px; 48 | background-image: image-url('feedback/feedback_tab_h.png'); 49 | _background-image: image-url('feedback/feedback_tab_h.png'); } } 50 | &#feedback_close_link { 51 | right: 5pt; 52 | top: 3pt; 53 | position: absolute; 54 | height: 22px; 55 | width: 66px; 56 | background-image: image-url('feedback/closelabel.gif'); 57 | _background-image: image-url('feedback/closelabel.gif'); } } 58 | 59 | div { 60 | &#feedback_modal_window { 61 | width: 300px; 62 | margin: 0 auto; 63 | position: relative; 64 | background-color: #FFFFFF; } 65 | &#feedback_modal_content, &#feedback_loading { 66 | padding: 12px; } } 67 | 68 | #feedback_overlay { 69 | position: fixed; 70 | top: 0px; 71 | left: 0px; 72 | height: 100%; 73 | width: 100%; 74 | background-color: #000; 75 | opacity: 0.5; 76 | -moz-opacity: 0.5; 77 | filter: alpha(opacity = 50); } 78 | 79 | .feedback_hide { 80 | z-index: -100; } 81 | 82 | .feedback_overlayBG { 83 | background-color: #000; 84 | z-index: 100000; } 85 | 86 | * html #feedback_overlay { 87 | /* ie6 hack */ 88 | position: absolute; 89 | height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); } 90 | 91 | form#feedback_form .error { 92 | color: red; 93 | font-weight: bold; } 94 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/feedback_form_generator.rb: -------------------------------------------------------------------------------- 1 | class FeedbackFormGenerator < Rails::Generators::Base 2 | source_root File.expand_path('../templates', __FILE__) 3 | 4 | argument :name, :type => :string, :default => "feedback" 5 | class_option :jquery, :type => :boolean, :default => true, :desc => "Use jQuery Javascript framework" 6 | 7 | attr_accessor :model_class_name, 8 | :controller_class_name, 9 | :helper_class_name, 10 | :mailer_class_name 11 | 12 | def initialize(*runtime_args, &runtime_options) 13 | super 14 | @name ||= "feedback" 15 | @model_class_name = name.classify 16 | @mailer_class_name = "#{@model_class_name}Mailer" 17 | @controller_class_name = "#{@model_class_name.pluralize}Controller" 18 | @helper_class_name = "#{@model_class_name.pluralize}Helper" 19 | end 20 | 21 | def add_stylesheet 22 | copy_file 'feedback.css.scss', 'app/assets/stylesheets/feedback.css.scss' 23 | end 24 | 25 | def add_javascript 26 | file_name = options[:jquery] ? 'jquery.feedback.js.coffee' : 'prototype.feedback.js.coffee' 27 | copy_file file_name, "app/assets/javascripts/#{file_name}" 28 | end 29 | 30 | def add_images 31 | copy_file "images/feedback_tab.png", "app/assets/images/feedback/feedback_tab.png" 32 | copy_file "images/feedback_tab_h.png", "app/assets/images/feedback/feedback_tab_h.png" 33 | copy_file "images/closelabel.gif", "app/assets/images/feedback/closelabel.gif" 34 | copy_file "images/loading.gif", "app/assets/images/feedback/loading.gif" 35 | end 36 | 37 | def add_model 38 | template 'feedback_model.rb.erb', "app/models/#{name}.rb" 39 | end 40 | 41 | def add_mailer 42 | template 'feedback_mailer.rb.erb', "app/mailers/#{name}_mailer.rb" 43 | copy_file 'views/feedback_mailer/feedback.html.erb', "app/views/#{name}_mailer/feedback.html.erb" 44 | end 45 | 46 | def add_controller 47 | template 'feedbacks_controller.rb.erb', "app/controllers/#{name.pluralize}_controller.rb" 48 | end 49 | 50 | def add_helper 51 | template_name = options[:jquery] ? 'feedbacks_helper.rb.jquery.erb' : 'feedbacks_helper.rb.prototype.erb' 52 | template template_name, "app/helpers/#{name.pluralize}_helper.rb" 53 | end 54 | 55 | def add_views 56 | copy_file 'views/feedbacks/new.html.erb', "app/views/#{name.pluralize}/new.html.erb" 57 | end 58 | 59 | def add_routes 60 | route "match 'feedbacks/new' => '#{name.pluralize}#new', :as => :new_feedback" 61 | route "match 'feedbacks' => '#{name.pluralize}#create', :as => :feedback" 62 | end 63 | 64 | def add_unit_test 65 | template 'feedback_test.rb.erb', "test/unit/#{name}_test.rb" 66 | template 'feedback_mailer_test.rb.erb', "test/unit/#{name}_mailer_test.rb" 67 | end 68 | 69 | def add_functional_test 70 | template 'feedbacks_controller_test.rb.erb', "test/functional/#{name.pluralize}_controller_test.rb" 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/feedback.css: -------------------------------------------------------------------------------- 1 | 2 | div#feedback { 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | z-index: 100001; 9 | } 10 | 11 | a#feedback_link { 12 | position: fixed; 13 | z-index: 99999; 14 | background-color: #CC0000; 15 | width: 26px; 16 | height: 96px; 17 | overflow: hidden; 18 | background: #C00 url(/images/feedback/feedback_tab.png); 19 | _position: absolute; 20 | /*_background-image: url(/images/feedback/feedback_tab.png);*/ 21 | } 22 | 23 | a#feedback_link:hover { 24 | background-color: #999; 25 | } 26 | 27 | a#feedback_link.left { 28 | left: 0; 29 | top: 25%; 30 | width: 26px; 31 | height: 96px; 32 | background-image: url(/images/feedback/feedback_tab.png); 33 | _background-image: url(/images/feedback/feedback_tab.png); 34 | } 35 | 36 | a#feedback_link.right { 37 | right: 0; 38 | top: 25%; 39 | width: 26px; 40 | height: 96px; 41 | background-image: url(/images/feedback/feedback_tab.png); 42 | _background-image: url(/images/feedback/feedback_tab.png); 43 | } 44 | 45 | a#feedback_link.top { 46 | right: 10%; 47 | top: 0; 48 | width: 96px; 49 | height: 26px; 50 | background-image: url(/images/feedback/feedback_tab_h.png); 51 | _background-image: url(/images/feedback/feedback_tab_h.png); 52 | } 53 | 54 | a#feedback_link.bottom { 55 | right: 10%; 56 | bottom: 0; 57 | width: 96px; 58 | height: 26px; 59 | background-image: url(/images/feedback/feedback_tab_h.png); 60 | _background-image: url(/images/feedback/feedback_tab_h.png); 61 | } 62 | 63 | a#feedback_close_link { 64 | right: 5pt; 65 | top: 3pt; 66 | position: absolute; 67 | height: 22px; 68 | width: 66px; 69 | background-image: url(/images/feedback/closelabel.gif); 70 | _background-image: url(/images/feedback/closelabel.gif); 71 | } 72 | 73 | div#feedback_modal_window { 74 | width: 300px; 75 | margin: 0 auto; 76 | position: relative; 77 | background-color: #FFFFFF; 78 | } 79 | 80 | div#feedback_modal_content { 81 | padding: 12px; 82 | } 83 | 84 | div#feedback_loading { 85 | padding: 12px; 86 | } 87 | 88 | #feedback_overlay { 89 | position: fixed; 90 | top: 0px; 91 | left: 0px; 92 | height: 100%; 93 | width: 100%; 94 | background-color: #000; 95 | opacity: 0.5; 96 | -moz-opacity: 0.5; 97 | filter:alpha(opacity=50); 98 | } 99 | 100 | .feedback_hide { 101 | z-index:-100; 102 | } 103 | 104 | .feedback_overlayBG { 105 | background-color: #000; 106 | z-index: 100000; 107 | } 108 | 109 | * html #feedback_overlay { /* ie6 hack */ 110 | position: absolute; 111 | height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); 112 | } 113 | 114 | form#feedback_form .error { 115 | color: red; 116 | font-weight: bold; 117 | } 118 | -------------------------------------------------------------------------------- /generators/feedback_form/feedback_form_generator.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/lib/insert_routes.rb") 2 | 3 | class FeedbackFormGenerator < Rails::Generator::Base 4 | 5 | attr_accessor :name, 6 | :model_class_name, 7 | :controller_class_name, 8 | :helper_class_name, 9 | :mailer_class_name 10 | 11 | 12 | def initialize(runtime_args, runtime_options = {}) 13 | super 14 | @name = (runtime_args[0] || "feedback").downcase 15 | @model_class_name = name.classify 16 | @mailer_class_name = "#{@model_class_name}Mailer" 17 | @controller_class_name = "#{@model_class_name.pluralize}Controller" 18 | @helper_class_name = "#{@model_class_name.pluralize}Helper" 19 | #@js_framework = (runtime_options['']) 20 | 21 | end 22 | 23 | def manifest 24 | record do |m| 25 | 26 | puts "hello" 27 | add_model(m) 28 | add_mailer(m) 29 | add_controller(m) 30 | add_helper(m) 31 | add_views(m) 32 | add_routes(m) 33 | add_unit_test(m) 34 | add_functional_test(m) 35 | add_stylesheet(m) 36 | add_javascript(m) 37 | add_images(m) 38 | end 39 | end 40 | 41 | 42 | def add_stylesheet(m) 43 | m.directory 'public/stylesheets' 44 | m.file 'feedback.css', 'public/stylesheets/feedback.css' 45 | 46 | end 47 | 48 | def add_javascript(m) 49 | m.directory 'public/javascripts' 50 | file_name = options[:jquery] ? 'jquery.feedback.js' : 'prototype.feedback.js' 51 | m.file file_name, "public/javascripts/#{file_name}" 52 | end 53 | 54 | def add_images(m) 55 | m.directory 'public/images/feedback' 56 | m.file "images/feedback_tab.png", "public/images/feedback/feedback_tab.png" 57 | m.file "images/feedback_tab_h.png", "public/images/feedback/feedback_tab_h.png" 58 | m.file "images/closelabel.gif", "public/images/feedback/closelabel.gif" 59 | m.file "images/loading.gif", "public/images/feedback/loading.gif" 60 | end 61 | 62 | def add_model(m) 63 | m.template 'feedback_model.rb.erb', "app/models/#{name}.rb" 64 | end 65 | 66 | def add_mailer(m) 67 | m.template 'feedback_mailer.rb.erb', "app/models/#{name}_mailer.rb" 68 | m.directory "app/views/#{name}_mailer" 69 | m.file 'views/feedback_mailer/feedback.html.erb', "app/views/#{name}_mailer/feedback.html.erb" 70 | 71 | end 72 | 73 | def add_controller(m) 74 | m.template 'feedbacks_controller.rb.erb', "app/controllers/#{name.pluralize}_controller.rb" 75 | end 76 | 77 | def add_helper(m) 78 | template_name = options[:jquery] ? 'feedbacks_helper.rb.jquery.erb' : 'feedbacks_helper.rb.prototype.erb' 79 | m.template template_name, "app/helpers/#{name.pluralize}_helper.rb" 80 | end 81 | 82 | def add_views(m) 83 | m.directory "app/views/#{name.pluralize}" 84 | m.file 'views/feedbacks/new.html.erb', "app/views/#{name.pluralize}/new.html.erb" 85 | end 86 | 87 | def add_routes(m) 88 | m.route_name "new_feedback", "feedbacks/new", {:controller => name.pluralize, :action => "new"} 89 | m.route_name "feedback", "feedbacks", {:controller => name.pluralize, :action => "create"} 90 | end 91 | 92 | def add_unit_test(m) 93 | m.template 'feedback_test.rb.erb', "test/unit/#{name}_test.rb" 94 | m.template 'feedback_mailer_test.rb.erb', "test/unit/#{name}_mailer_test.rb" 95 | end 96 | 97 | def add_functional_test(m) 98 | m.template 'feedbacks_controller_test.rb.erb', "test/functional/#{name.pluralize}_controller_test.rb" 99 | end 100 | 101 | protected 102 | 103 | def add_options!(opt) 104 | opt.separator '' 105 | opt.separator 'Options:' 106 | opt.on("--jquery", 107 | "Use jquery Javascript framework, default is Prototyp") { |v| options[:jquery] = true } 108 | end 109 | end -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/jquery.feedback.js.coffee: -------------------------------------------------------------------------------- 1 | (($) -> 2 | settings = undefined 3 | $.fn.feedback = (callerSettings) -> 4 | settings = $.extend( 5 | main: "feedback" 6 | closeLink: "feedback_close_link" 7 | modalWindow: "feedback_modal_window" 8 | modalContent: "feedback_modal_content" 9 | form: "feedback_form" 10 | formUrl: "/feedbacks/new" 11 | overlay: "feedback_overlay" 12 | loadingImage: "/assets/feedback/loading.gif" 13 | loadingText: "Loading..." 14 | sendingText: "Sending..." 15 | tabPosition: "left" 16 | , callerSettings or {}) 17 | settings.feedbackHtml = "
" + "
" + "" + "
" + "
" + "
" 18 | settings.overlayHtml = "
" 19 | settings.tabHtml = "" 20 | settings.main = "#" + settings.main 21 | settings.closeLink = "#" + settings.closeLink 22 | settings.modalWindow = "#" + settings.modalWindow 23 | settings.modalContent = "#" + settings.modalContent 24 | settings.form = "#" + settings.form 25 | settings.overlay = "#" + settings.overlay 26 | settings.tabControls = this 27 | if settings.tabPosition? and $("#feedback_link").length is 0 28 | $("body").append settings.tabHtml 29 | settings.tabControls = $(settings.tabControls).add($("#feedback_link")) 30 | $(settings.tabControls).click -> 31 | loading() 32 | $(settings.modalContent).load settings.formUrl, null, -> 33 | $(settings.form).submit submitFeedback 34 | 35 | false 36 | 37 | submitFeedback = -> 38 | $("input[name=feedback\\[page\\]]").val location.href 39 | data = $(settings.form).serialize() 40 | url = $.trim($(settings.form).attr("action")) 41 | loading settings.sendingText 42 | $.ajax 43 | type: "POST" 44 | url: url 45 | data: data 46 | success: (msg, status) -> 47 | $(settings.modalContent).html msg 48 | $(settings.modalWindow).fadeOut 2000, -> 49 | hideFeedback() 50 | 51 | error: (xhr, status, a) -> 52 | $(settings.modalContent).html xhr.responseText 53 | $(settings.form).submit submitFeedback 54 | 55 | false 56 | 57 | initOverlay = -> 58 | $("body").append settings.overlayHtml if $(settings.overlay).length is 0 59 | $(settings.overlay).hide().addClass "feedback_overlayBG" 60 | 61 | showOverlay = -> 62 | initOverlay().show() 63 | 64 | hideOverlay = -> 65 | return false if $(settings.overlay).length is 0 66 | $(settings.overlay).remove() 67 | 68 | initFeedback = -> 69 | if $(settings.main).length is 0 70 | $("body").append settings.feedbackHtml 71 | $(settings.closeLink).click -> 72 | hideFeedback() 73 | false 74 | 75 | setBoxPosition() 76 | $ settings.main 77 | 78 | showFeedback = -> 79 | initFeedback().show() 80 | 81 | hideFeedback = -> 82 | $(settings.main).hide() 83 | $(settings.main).remove() 84 | hideOverlay() 85 | 86 | setBoxPosition = -> 87 | scrollTop = undefined 88 | clientHeight = undefined 89 | if self.pageYOffset 90 | scrollTop = self.pageYOffset 91 | else if document.documentElement and document.documentElement.scrollTop 92 | scrollTop = document.documentElement.scrollTop 93 | else scrollTop = document.body.scrollTop if document.body 94 | if self.innerHeight 95 | clientHeight = self.innerHeight 96 | else if document.documentElement and document.documentElement.clientHeight 97 | clientHeight = document.documentElement.clientHeight 98 | else clientHeight = document.body.clientHeight if document.body 99 | $(settings.modalWindow).css top: scrollTop + (clientHeight / 10) + "px" 100 | 101 | loading = (text) -> 102 | showOverlay() 103 | initFeedback() 104 | text = settings.loadingText unless text? 105 | $(settings.modalContent).html "

" + text + "

" 106 | showFeedback() 107 | ) jQuery 108 | -------------------------------------------------------------------------------- /lib/generators/feedback_form/templates/prototype.feedback.js.coffee: -------------------------------------------------------------------------------- 1 | Feedback = undefined 2 | if Feedback is `undefined` 3 | Feedback = {} 4 | Feedback.init = (callerSettings) -> 5 | @settings = Object.extend( 6 | tabControl: "feedback_link" 7 | main: "feedback" 8 | closeLink: "feedback_close_link" 9 | modalWindow: "feedback_modal_window" 10 | modalContent: "feedback_modal_content" 11 | form: "feedback_form" 12 | formUrl: "/feedbacks/new" 13 | overlay: "feedback_overlay" 14 | loadingImage: "/assets/feedback/loading.gif" 15 | loadingText: "Loading..." 16 | sendingText: "Sending..." 17 | tabPosition: "left" 18 | , callerSettings or {}) 19 | @settings.feedbackHtml = "
" + "
" + "" + "
" + "
" + "
" 20 | @settings.overlayHtml = "
" 21 | @settings.tabHtml = "" 22 | $$("body").first().insert @settings.tabHtml if @settings.tabPosition? and $$("#" + @settings.tabControl).length is 0 23 | $$("." + @settings.tabControl).each (e) -> 24 | $(e).observe "click", -> 25 | Feedback.loading() 26 | new Ajax.Updater(Feedback.settings.modalContent, Feedback.settings.formUrl, 27 | method: "get" 28 | onComplete: (transport) -> 29 | $(Feedback.settings.form).observe "submit", Feedback.submitFeedback 30 | ) 31 | false 32 | 33 | Feedback.submitFeedback = (event) -> 34 | $("feedback_page").value = location.href 35 | data = Form.serialize($(Feedback.settings.form)) 36 | url = $(Feedback.settings.form).action 37 | Feedback.loading Feedback.settings.sendingText 38 | new Ajax.Updater(Feedback.settings.modalContent, url, 39 | method: "POST" 40 | parameters: data 41 | onComplete: (transport) -> 42 | if transport.status >= 200 and transport.status < 300 43 | $(Feedback.settings.modalWindow).fade 44 | duration: 2.0 45 | afterFinish: -> 46 | Feedback.hideFeedback() 47 | else 48 | $(Feedback.settings.form).observe "submit", Feedback.submitFeedback 49 | ) 50 | Event.stop event 51 | 52 | Feedback.initOverlay = -> 53 | $$("body").first().insert @settings.overlayHtml if $$("#" + @settings.overlay).length is 0 54 | $(@settings.overlay).addClassName "feedback_overlayBG" 55 | 56 | Feedback.showOverlay = -> 57 | Feedback.initOverlay() 58 | $(@settings.overlay).show() 59 | 60 | Feedback.hideOverlay = -> 61 | return false if $$("#" + @settings.overlay).length is 0 62 | $(@settings.overlay).remove() 63 | 64 | Feedback.initFeedback = -> 65 | if $$("#" + @settings.main).length is 0 66 | $$("body").first().insert @settings.feedbackHtml 67 | $(@settings.closeLink).observe "click", -> 68 | Feedback.hideFeedback() 69 | false 70 | 71 | Feedback.setWindowPosition() 72 | 73 | Feedback.showFeedback = -> 74 | Feedback.initFeedback() 75 | $(@settings.main).show() 76 | 77 | Feedback.hideFeedback = -> 78 | $(@settings.main).hide() 79 | $(@settings.main).remove() 80 | Feedback.hideOverlay() 81 | 82 | Feedback.loading = (text) -> 83 | Feedback.showOverlay() 84 | Feedback.initFeedback() 85 | text = @settings.loadingText unless text? 86 | $(@settings.modalContent).update "

" + text + "

" 87 | $(@settings.main).show() 88 | 89 | Feedback.setWindowPosition = -> 90 | scrollTop = undefined 91 | clientHeight = undefined 92 | if self.pageYOffset 93 | scrollTop = self.pageYOffset 94 | else if document.documentElement and document.documentElement.scrollTop 95 | scrollTop = document.documentElement.scrollTop 96 | else scrollTop = document.body.scrollTop if document.body 97 | if self.innerHeight 98 | clientHeight = self.innerHeight 99 | else if document.documentElement and document.documentElement.clientHeight 100 | clientHeight = document.documentElement.clientHeight 101 | else clientHeight = document.body.clientHeight if document.body 102 | $(@settings.modalWindow).setStyle top: scrollTop + (clientHeight / 10) + "px" 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Feedback Plugin for Rails 2 | 3 | Feedback is a Ruby on Rails plugin that adds an ajax-based 4 | overlay feedback form triggered by a side-screen tab or a link. 5 | The result of the feedback form is sent through a ActionMailer. 6 | 7 | The plugin was inspired from the "feedback" widget of 8 | GetSatisfaction.com and UserVoice.com. 9 | 10 | The plugin uses a generator to generate boiler-plate Javascript, 11 | CSS, HTML, and ruby code into your application. 12 | It sets up a ready to use feedback form (with minimal styling) 13 | that you can further customize. 14 | 15 | 16 | ## Features 17 | 18 | * Helper to display a sticky "feedback" tab. 19 | * Display feedback form as an overlay with Ajax. 20 | * Sends feedback result by email through an ActionMailer. 21 | * Uses a generator such that you can customize the code and style to fit your app. 22 | * Generates unit and functional tests. 23 | * Works with Prototype and JQuery, choose through a generators flag. 24 | * Currently supports IE6, IE7, IE8, FF2, FF3, Chrome2 and SafariX 25 | 26 | 27 | ## Motivation 28 | 29 | Collecting feedback from users is really important. Third-party services 30 | such as GetSatisfaction.com and UserVoice.com are awesome tools to manage your feedback. 31 | However, we found out that in many instances such as for small sites or back-ends, we wanted 32 | a similar feedback form integration/experience but simpler, local, and customized. 33 | 34 | 35 | ## Install and Getting Started 36 | 37 | ### for Rails 2.x 38 | 39 | Install the plugin: 40 | 41 | ruby script/plugin install git://github.com/jsboulanger/feedback.git 42 | 43 | Run the generator: 44 | 45 | ruby script/generate feedback_form 46 | 47 | By default it will generate the Prototype code, you can use jQuery if you prefer: 48 | 49 | ruby script/generate feedback_form --jquery 50 | 51 | Include the feedback.css and the jquery.feedback.js/prototype.feedback.js files into your header. 52 | There is a helper to do that: 53 | 54 | <%= feedback_includes %> 55 | 56 | ### for Rails 3.x 57 | 58 | Install the generator: 59 | 60 | git clone git://github.com/dohkoos/feedback.git 61 | cp feedback/lib/generators/feedback_form [YOUR_PROJECT_NAME]/lib/generators 62 | 63 | Run the generator: 64 | 65 | rails generate feedback_form 66 | 67 | By default it will generate the jQuery code, you can use Prototype if you prefer: 68 | 69 | rails generate feedback_form --skip-jquery 70 | 71 | To add a sticky 'feedback' tab to your site, in the header place: 72 | 73 | <%= feedback_tab(:position => 'top') %> 74 | 75 | Configure the action mailer in app/models/feedback_mailer.rb 76 | 77 | def feedback(feedback) 78 | @recipients = 'you@yoursite.com' 79 | @from = 'system@yoursite.com' 80 | @subject = "[Feedback for YourSite.com] #{feedback.subject}" 81 | ... 82 | 83 | 84 | ## How-to's 85 | 86 | ### How to customize the position of the feedback tab? 87 | 88 | The feedback_tab helper takes the option "position" which has four different values (top|bottom|left|right) 89 | that corresponds to class in the feedback.css file. You can fine tune the position by changing the left/right, top/bottom 90 | attributes of those css classes. 91 | 92 | ### How to trigger the feedback form from a custom link? 93 | 94 | The feedback_link(text, options={}) helper allows you the create a link that will trigger the feedback form. 95 | 96 | <%= feedback_link "Leave us feedback!" %> 97 | 98 | Your layout header must also have the includes: 99 | 100 | <%= feedback_includes %> 101 | 102 | 103 | ### How to customize the email message? 104 | 105 | Edit the /app/views/feedback_mailer/feedback.html.erb generated file in your app 106 | 107 | 108 | ### How to store the feedback results in the database? 109 | 110 | Feedback generates a model that is not an active record model. In order to store the feedbacks in your database, make it 111 | an active record model: 112 | 113 | class Feedback < ActiveRecord::Base 114 | attr_accessible :comment, :email, :subject, :page 115 | 116 | validates :subject, :presence => true 117 | validates :comment, :presence => true 118 | end 119 | 120 | You must also write a migration to create your table: 121 | 122 | class CreateFeedbacks < ActiveRecord::Migration 123 | def self.up 124 | create_table :feedbacks do |t| 125 | t.string :subject 126 | t.string :email 127 | t.text :comment 128 | t.timestamps 129 | end 130 | end 131 | 132 | def self.down 133 | drop_table :feedbacks 134 | end 135 | end 136 | 137 | 138 | ## Limitations (TODOs) 139 | 140 | * No unit tests for Javascript 141 | * Feedback PNG with transparency supported by IE6 142 | * Graceful fallback if Javascript is not enabled/supported 143 | * More customization options through helpers: (overlay style, tab color, etc...) 144 | 145 | 146 | ## Acknowledgements 147 | 148 | Thanks to GetSatisfaction.com for the 149 | idea of the "feedback" tab. 150 | 151 | Part of this plugin was inspired from other software. 152 | Thanks to their respective authors. 153 | * Facebox: http://famspam.com/facebox 154 | * restful_authentication: http://github.com/technoweenie/restful-authentication/tree/master 155 | 156 | Copyright (c) 2009 Jean-Sebastien Boulanger , released under the MIT license 157 | 158 | http://jsboulanger.com 159 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/jquery.feedback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Feedback (for jQuery) 3 | * version: 0.1 (2009-07-21) 4 | * @requires jQuery v1.3 or later 5 | * 6 | * This script is part of the Feedback Ruby on Rails Plugin: 7 | * http:// 8 | * 9 | * Licensed under the MIT: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * 12 | * Copyright 2009 Jean-Sebastien Boulanger [ jsboulanger@gmail.com ] 13 | * 14 | * Usage: 15 | * 16 | * jQuery(document).ready(function() { 17 | * jQuery('#feedback_tab_link').feedback({ 18 | * // options 19 | * }); 20 | * }) 21 | * 22 | */ 23 | (function($) { 24 | var settings; 25 | 26 | $.fn.feedback = function(callerSettings) { 27 | settings = $.extend({ 28 | main: 'feedback', 29 | closeLink: 'feedback_close_link', 30 | modalWindow: 'feedback_modal_window', 31 | modalContent: 'feedback_modal_content', 32 | form: 'feedback_form', 33 | formUrl: '/feedbacks/new', 34 | overlay: 'feedback_overlay', 35 | loadingImage: '/images/feedback/loading.gif', 36 | loadingText: 'Loading...', 37 | sendingText: 'Sending...', 38 | tabPosition: 'left' 39 | }, callerSettings || {}); 40 | 41 | settings.feedbackHtml = '' 47 | settings.overlayHtml = ''; 48 | settings.tabHtml = ''; 49 | settings.main = '#' + settings.main; 50 | settings.closeLink = '#' + settings.closeLink; 51 | settings.modalWindow = '#' + settings.modalWindow; 52 | settings.modalContent = '#' + settings.modalContent; 53 | settings.form = '#' + settings.form; 54 | settings.overlay = '#' + settings.overlay; 55 | settings.tabControls = this; 56 | 57 | if (settings.tabPosition != null && $("#feedback_link").length == 0) { 58 | $("body").append(settings.tabHtml); 59 | settings.tabControls = $(settings.tabControls).add($('#feedback_link')); 60 | } 61 | 62 | $(settings.tabControls).click(function() { 63 | loading(); 64 | $(settings.modalContent).load(settings.formUrl, null, function() { 65 | $(settings.form).submit(submitFeedback); 66 | }); 67 | return false; 68 | }); 69 | }; 70 | 71 | var submitFeedback = function() { 72 | $('input[name=feedback\\[page\\]]').val(location.href); 73 | var data = $(settings.form).serialize(); 74 | var url = $.trim($(settings.form).attr('action')); 75 | loading(settings.sendingText); 76 | $.ajax({ 77 | type: "POST", 78 | url: url, 79 | data: data, 80 | success: function(msg, status) { 81 | $(settings.modalContent).html(msg); 82 | $(settings.modalWindow).fadeOut(2000, function() { 83 | hideFeedback(); 84 | }); 85 | }, 86 | error: function(xhr, status, a) { 87 | $(settings.modalContent).html(xhr.responseText); 88 | $(settings.form).submit(submitFeedback); 89 | } 90 | }); 91 | return false; 92 | } 93 | 94 | 95 | var initOverlay = function() { 96 | if ($(settings.overlay).length == 0) 97 | $("body").append(settings.overlayHtml) 98 | return $(settings.overlay).hide().addClass("feedback_overlayBG") 99 | } 100 | 101 | var showOverlay = function() { 102 | initOverlay().show(); 103 | } 104 | 105 | var hideOverlay = function() { 106 | if ($(settings.overlay).length == 0) return false; 107 | $(settings.overlay).remove(); 108 | } 109 | 110 | var initFeedback = function() { 111 | if ($(settings.main).length == 0) { 112 | $("body").append(settings.feedbackHtml); 113 | $(settings.closeLink).click(function() { 114 | hideFeedback(); 115 | return false; 116 | }); 117 | setBoxPosition(); 118 | } 119 | return $(settings.main); 120 | } 121 | 122 | var showFeedback = function() { 123 | initFeedback().show(); 124 | } 125 | 126 | var hideFeedback = function() { 127 | $(settings.main).hide(); 128 | $(settings.main).remove(); 129 | hideOverlay(); 130 | } 131 | 132 | var setBoxPosition = function() { 133 | var scrollTop, clientHeight; 134 | if (self.pageYOffset) { 135 | scrollTop = self.pageYOffset; 136 | } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict 137 | scrollTop = document.documentElement.scrollTop; 138 | } else if (document.body) {// all other Explorers 139 | scrollTop = document.body.scrollTop; 140 | } 141 | if (self.innerHeight) { // all except Explorer 142 | clientHeight = self.innerHeight; 143 | } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 144 | clientHeight = document.documentElement.clientHeight; 145 | } else if (document.body) { // other Explorers 146 | clientHeight = document.body.clientHeight; 147 | } 148 | $(settings.modalWindow).css({ 149 | top: scrollTop + (clientHeight / 10) + 'px' 150 | }); 151 | } 152 | 153 | var loading = function(text) { 154 | showOverlay(); 155 | initFeedback(); 156 | 157 | if(text == null) 158 | text = settings.loadingText; 159 | 160 | $(settings.modalContent).html( 161 | '

' + text + '

'); 162 | 163 | showFeedback(); 164 | 165 | } 166 | }) (jQuery); 167 | 168 | -------------------------------------------------------------------------------- /generators/feedback_form/templates/prototype.feedback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Feedback (for Prototype) 3 | * version: 0.1 (2009-07-21) 4 | * @requires Prototype v1.6 or later 5 | * 6 | * This script is part of the Feedback Ruby on Rails Plugin: 7 | * http:// 8 | * 9 | * Licensed under the MIT: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * 12 | * Copyright 2009 Jean-Sebastien Boulanger [ jsboulanger@gmail.com ] 13 | * 14 | * Usage: 15 | * 16 | * Feedback.init('feedback_link_tab', { 17 | * // options 18 | * }); 19 | * 20 | */ 21 | 22 | var Feedback; 23 | 24 | if(Feedback == undefined) { 25 | Feedback = {}; 26 | Feedback.init = function(callerSettings) { // Changes to those default settings might need adjustment in feedback.css 27 | this.settings = Object.extend({ 28 | tabControl: 'feedback_link', 29 | main: 'feedback', 30 | closeLink: 'feedback_close_link', 31 | modalWindow: 'feedback_modal_window', 32 | modalContent: 'feedback_modal_content', 33 | form: 'feedback_form', 34 | formUrl: '/feedbacks/new', 35 | overlay: 'feedback_overlay', 36 | loadingImage: '/images/feedback/loading.gif', 37 | loadingText: 'Loading...', 38 | sendingText: 'Sending...', 39 | tabPosition: 'left' 40 | }, callerSettings || {}); 41 | 42 | this.settings.feedbackHtml = '' 48 | this.settings.overlayHtml = ''; 49 | this.settings.tabHtml = ''; 50 | 51 | if (this.settings.tabPosition != null && $$('#' + this.settings.tabControl).length == 0) 52 | $$("body").first().insert(this.settings.tabHtml); 53 | 54 | $$('.' + this.settings.tabControl).each(function(e) { 55 | $(e).observe('click', function() { 56 | Feedback.loading(); 57 | new Ajax.Updater(Feedback.settings.modalContent, Feedback.settings.formUrl, { 58 | method: 'get', 59 | onComplete: function(transport) { 60 | $(Feedback.settings.form).observe('submit', Feedback.submitFeedback); 61 | } 62 | }); 63 | return false; 64 | }); 65 | }); 66 | } 67 | 68 | Feedback.submitFeedback = function(event){ 69 | $('feedback_page').value = location.href; 70 | var data = Form.serialize($(Feedback.settings.form)); 71 | var url = $(Feedback.settings.form).action; 72 | Feedback.loading(Feedback.settings.sendingText); 73 | new Ajax.Updater(Feedback.settings.modalContent, url, { 74 | method: 'POST', 75 | parameters: data, 76 | onComplete: function(transport){ 77 | if (transport.status >= 200 && transport.status < 300) { 78 | $(Feedback.settings.modalWindow).fade({ 79 | duration: 2.0, 80 | afterFinish: function() { 81 | Feedback.hideFeedback(); 82 | } 83 | }); 84 | } 85 | else { 86 | $(Feedback.settings.form).observe('submit', Feedback.submitFeedback); 87 | } 88 | } 89 | }); Event.stop(event); 90 | } 91 | 92 | Feedback.initOverlay = function() { 93 | if ($$('#' + this.settings.overlay).length == 0) 94 | $$("body").first().insert(this.settings.overlayHtml) 95 | 96 | $(this.settings.overlay).addClassName('feedback_overlayBG'); 97 | } 98 | 99 | Feedback.showOverlay = function() { 100 | Feedback.initOverlay(); 101 | $(this.settings.overlay).show(); 102 | } 103 | 104 | Feedback.hideOverlay = function() { 105 | if ($$('#' + this.settings.overlay).length == 0) return false; 106 | $(this.settings.overlay).remove(); 107 | } 108 | 109 | Feedback.initFeedback = function() { 110 | if ($$('#' + this.settings.main).length == 0) { 111 | $$("body").first().insert(this.settings.feedbackHtml); 112 | $(this.settings.closeLink).observe('click', function(){ 113 | Feedback.hideFeedback(); 114 | return false; 115 | }); 116 | Feedback.setWindowPosition(); 117 | } 118 | } 119 | 120 | Feedback.showFeedback = function() { 121 | Feedback.initFeedback(); 122 | $(this.settings.main).show(); 123 | } 124 | 125 | Feedback.hideFeedback = function() { 126 | $(this.settings.main).hide(); 127 | $(this.settings.main).remove(); 128 | Feedback.hideOverlay(); 129 | } 130 | 131 | 132 | Feedback.loading = function(text){ 133 | Feedback.showOverlay(); 134 | Feedback.initFeedback(); 135 | if (text == null) 136 | text = this.settings.loadingText; 137 | 138 | $(this.settings.modalContent).update('

' + text + '

'); 139 | $(this.settings.main).show(); 140 | } 141 | 142 | Feedback.setWindowPosition = function() { 143 | var scrollTop, clientHeight; 144 | if (self.pageYOffset) { 145 | scrollTop = self.pageYOffset; 146 | } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict 147 | scrollTop = document.documentElement.scrollTop; 148 | } else if (document.body) {// all other Explorers 149 | scrollTop = document.body.scrollTop; 150 | } 151 | if (self.innerHeight) { // all except Explorer 152 | clientHeight = self.innerHeight; 153 | } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 154 | clientHeight = document.documentElement.clientHeight; 155 | } else if (document.body) { // other Explorers 156 | clientHeight = document.body.clientHeight; 157 | } 158 | $(this.settings.modalWindow).setStyle({ 159 | top: scrollTop + (clientHeight / 10) + 'px' 160 | }); 161 | } 162 | } 163 | --------------------------------------------------------------------------------