Rails collection select when collection is way big - ruby-on-rails

I have a form partial inside which I select associated users through a multiple: true collection select:
= f.collection_select(:user_ids, User.all, :id, :email, {selected: #limit_group.user_ids, include_blank: true}, {multiple: true, "data-placeholder" => "Add users to group"})
But how can I do this more efficiently to avoid big load times when the database has like thousands of users?

You'll be better using something called AutoComplete / LiveSearch with a text box (like Pardeep Saini mentioned).
We've done this before:
You could achieve this relatively simply:
= f.text_field :user_ids, placeholder: "Search for users"
You'd then have to use javascript:
#app/assets/javascripts/application.js
$(document).on("keyup", "input[type=text]#user_ids", function(){
$.getJSON("users/search", {name: $(this).val()}).done(function(json){
var users = [];
$.each(json.users, function(user) {
users.push("" + user.name + "");
});
$(".search").html(users).show();
});
});
$(document).on("click", ".search a", function(e) {
e.preventDefault();
// add hidden field with user name to form
});
You'd have to back it up with the relevant controller action:
#config/routes.rb
resources :users do
get :search, on: :collection
end
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def search
#users = User.where("name LIKE ?", "%" + params[:name] + "%")
respond_to do |format|
format.json (render json: #users.to_json)
end
end
end
The above code should be refactored.
--
To get this working with multiple values would be a little bit more involved. It could be done, but you'd have to do it like the tags setup in StackOverflow...
The way they do that is to basically use a similar principle to the above (each tag will be a returned piece of data from the search).
Here's the actual code we used in the cosmetics example above:
#app/assets/javascripts/extra/jquery.livesearch.js
(function($) {
$.searchbox = {}
$.extend(true, $.searchbox, {
settings: {
url: 'search',
param: 'search',
dom_id: '#livesearch',
minChars: 2,
loading_css: '#livesearch_loading',
del_id: '#livesearch_del'
},
loading: function() {
$($.searchbox.settings.loading_css).show()
},
idle: function() {
$($.searchbox.settings.loading_css).hide()
},
start: function() {
$.searchbox.loading()
$(document).trigger('before.searchbox')
},
stop: function() {
$.searchbox.idle()
$(document).trigger('after.searchbox')
},
kill: function() {
$($.searchbox.settings.dom_id).fadeOut(50)
$($.searchbox.settings.dom_id).html('')
$($.searchbox.settings.del_id).fadeOut(100)
},
reset: function() {
$($.searchbox.settings.dom_id).html('')
$($.searchbox.settings.dom_id).fadeOut(50)
$('#SearchSearch').val('')
$($.searchbox.settings.del_id).fadeOut(100)
},
process: function(terms) {
if(/\S/.test(terms)) {
$.ajax({
type: 'GET',
url: $.searchbox.settings.url,
data: {search: terms.trim()},
complete: function(data) {
$($.searchbox.settings.del_id).fadeIn(50)
$($.searchbox.settings.dom_id).html(data.responseText)
if (!$($.searchbox.settings.dom_id).is(':empty')) {
$($.searchbox.settings.dom_id).fadeIn(100)
}
$.searchbox.stop();
}
});
return false;
}else{
$.searchbox.kill();
}
}
});
$.fn.searchbox = function(config) {
var settings = $.extend(true, $.searchbox.settings, config || {})
$(document).trigger('init.searchbox')
$.searchbox.idle()
return this.each(function() {
var $input = $(this)
$input
.keyup(function() {
if ($input.val() != this.previousValue) {
if(/\S/.test($input.val().trim()) && $input.val().trim().length > $.searchbox.settings.minChars){
$.searchbox.start()
$.searchbox.process($input.val())
}else{
$.searchbox.kill()
}
this.previousValue = $input.val()
}
})
})
}
})(jQuery);
... and ...
#app/assets/javascripts/application.js
$(document).ready( function() {
var base_url = window.location.protocol + "//" + window.location.host;
$('#SearchSearch').searchbox({
url: base_url + '/search/',
param: 'search',
dom_id: '#livesearch',
loading_css: '#livesearch_loading'
})
});
$(document).on('click', '#livesearch_del', function() { $.searchbox.reset(); })
$(document).on('submit', '#SearchForm', function() { $.searchbox.kill(); });
$(document).on('click', '.livesearch_results tr', function() { window.location = $('a:first', this).attr('href'); });
The routes & controller:
#config/routes.rb
match 'search(/:search)', :to => 'products#search', :as => :search, via: [:get, :post]
#app/models/product.rb
class Product < ActiveRecord::Base
def self.search(search)
where("name LIKE ? OR description LIKE ?", "%#{search}%", "%#{search}%").take(5)
end
end
#app/controllers/product_controller.rb
class ProductsController < ApplicationController
def search
#products = Product.search params[:search]
respond_to do |format|
format.js { render :partial => "elements/livesearch", :locals => {:search => #products, :query => params[:search]} }
format.html {
render :index
}
end
end
end
The views:
#app/views/elements/_livesearch.html.erb
<div class="livesearch_container">
<table class="livesearch_results">
<% unless search.blank? %>
<% search.each_with_index do |item,i| %>
<% pos ||= '' %>
<% if (i == 0) then pos = 'first' end %>
<% if (i == search.size - 1) then pos += ' last' end %>
<tr data-link="<%= "/#{item.slug}" %>" class="<%= "#{pos}" %>">
<td class="image">
<% model = item.images.first || item.images.build %>
<%= image_tag(model.image.url(:thumb), :title => item.name, data: {"placement" => "left"}, :height => "85") %><br/>
</td>
<td class="information">
<%= link_to image_tag(item.brand.images.first.image.url(:thumb), :width => "55", :title => "View #{item.brand.name}"), "/#{item.brand.slug}", :class => "brand" if defined?(item.brand.images.first) %>
<div class="name"><%= link_to item.name, "/#{item.slug}" %></div>
</td>
<td class="price">
<%= number_to_currency(item.price, unit: "£") %>
</td>
</tr>
<% end %>
<tr class="results"><td colspan="3"><%= link_to "See all #{search.size} results here »", search_path(query) %></td></tr>
<% else %>
<tr class="results"><td colspan="3"><%= link_to 'No results found', search_path(query) %></td></tr>
<% end %>
</table>
</div>
I also made a gist here: https://gist.github.com/richpeck/2310ff3ab1ffcd6a9138

Related

Undefined method `user_search_admin_path' for #<ActionView::Base:0x0000000010f748>

Getting this error when clicking on my Admin page:
"undefined method `user_search_admin_path' for #ActionView::Base:0x0000000010f748" on line 47 url: '<%= user_search_admin_path(:format=>:json) %>'.
I have a route for the "user_search" so not sure what is causing this error. Any ideas how to solve this error?
This is the admin route:
resources :admin, :as => :admin, :only => [:index, :create, :destroy] do
collection {
get "user_search";
get "group_search";
post "toggle_logging";
post "toggle_privs";
get "export_permissions";
get "export_roles";
};
Below are my code files:
index.html.erb
<% content_for :crumbs do %>
<li class="last"><%= link_to("Administrators", admin_index_path) %></li>
<% end %>
<% content_for :javascripts do %>
<script type="text/javascript">
function toggleGroup(group, that){
var el = "." + group;
$(el).parent().toggle(0,"swing",function(){
});
}
function highlightGroup(group, role){
var even = $('tr td.' + group).parent('.even').children("."+role).css("background-color");
var odd = $('tr td.' + group).parent('.odd').children("."+role).css("background-color");
// $('tr td.' + group).parent('.even').children("."+role).animate({backgroundColor:"red", opacity:0.5},800, function(){
// $('tr td.' + group).parent('.even').children("."+role).css({"background-color": even, opacity:1.0});
// });
// $('tr td.' + group).parent('.odd').children("."+role).animate({backgroundColor:"red", opacity:0.5},800, function(){
// $('tr td.' + group).parent('.odd').children("."+role).css({"background-color": odd, opacity:1.0});
// });
$('tr td.' + group).parent('.odd').children('.indented_description').animate({backgroundColor:"red", opacity:0.5},800, function(){
$('tr td.' + group).parent('.odd').children('.indented_description').css({"background-color": odd, opacity:1.0});
});
$('tr td.' + group).parent('.even').children('.indented_description').animate({backgroundColor:"red", opacity:0.5},800, function(){
$('tr td.' + group).parent('.even').children('.indented_description').css({"background-color": even, opacity:1.0});
});
}
$(document).ready(autocomplete_users);
function autocomplete_users() {
$(".add_usernames").autocomplete({
minLength: 3,
source: function(request, response) {
var copy = this.element;
$.ajax({
beforeSend: function(){
$(copy).parent().siblings(".spinner_td").css("display", "block");
},
complete: function() {
$(copy).parent().siblings(".spinner_td").css("display", "none");
},
url: '<%= user_search_admin_path(:format=>:json) %>',
data: {
q: request.term,
},
dataType: "json",
success: function(data) {
response(data);
},
})
},
parse: function(data) {
var parsed = [];
for ( var i = 0; i < data.length; ++i ) {
var row = data[i];
parsed[parsed.length] = {
data: row,
value: row,
result: row
};
}
return parsed;
}
});
}
$(document).ready(autocomplete_groups);
function autocomplete_groups() {
$(".add_groups").autocomplete({
minLength: 3,
source: function(request, response) {
var copy = this.element;
$.ajax({
beforeSend: function(){
$(copy).parent().siblings(".spinner_td").css("display", "block");
},
complete: function() {
$(copy).parent().siblings(".spinner_td").css("display", "none");
},
url: '<%= group_search_admin_path(:format=>:json) %>',
data: {
q: request.term,
},
dataType: "json",
success: function(data) {
response(data);
},
})
},
parse: function(data) {
var parsed = [];
for ( var i = 0; i < data.length; ++i ) {
var row = data[i];
parsed[parsed.length] = {
data: row,
value: row,
result: row
};
}
return parsed;
}
});
}
function add_new_group() {
$.ajax({
url:'auth_role/auth_group_add',
success: function(result) {
//$(result).insertBefore('#add_new_group_button')
$('.group_table').append(result);
autocomplete_groups();
}
});
};
function add_user_to_role() {
$.ajax({
url:'auth_role/auth_user_add',
success: function(result) {
//$(result).insertBefore('#add_new_user_button')
$('.user_table').append(result);
autocomplete_users();
}
});
};
function render_auth_role_partial () {
$.ajax({
url: 'auth_role/' + $("#roles_select").val()+ '/auth_role_partial',
success: function(result) {
$("#edit_role_div").replaceWith(result);
$("#roles_select").value = "<%= #role.id %>"
}
});
};
function show_new_role_form() {
$("#selector_div").hide();
$.ajax({
url: 'auth_role/new',
success: function(result) {
$("#edit_role_div").replaceWith(result);
}
});
};
// so users cannot lock themselves out of managing users
$(document).ready(function() {
$('#perms_checkbox_form').submit(function () {
if ($("input[id^='perm_role_task:manage_user_group']:checked").length == 0) {
alert("There must be at least one role with permission to manage users.");
return false;
}
});
});
function check_duplicate_role() {
var new_name = $("input[id='name']").val();
$.ajax({
url: 'auth_role/check_duplicate_role',
dataType: "json",
contentType: "application/json; charset=utf-8",
data: {new_name: new_name},
success: function(data) {
if (data == true) {
validate_role_form();
} else {
alert("Role already exists or is empty. Please choose another name.");
}
}
});
}
function validate_role_form() {
// Check that the role name is alphanumeric
var role_name = $('#edit_role_div input[name="name"]').val();
if (/[^\w\s]+/.test(role_name)) {
alert("Role name can only contain letters, numbers, _, or whitespace");
$('#edit_role_div input[name="name"]').css("background-color", "#FFB2B2");
return;
}
if ($.trim(role_name).length < 1) {
alert("Role name cannot be blank");
$('#edit_role_div input[name="name"]').css("background-color", "#FFB2B2");
return;
}
var groups = ($("input[id='auth_groups_name']"));
var users = ($("input[id='users_']"));
var group_names = [];
var user_names= [];
var submit_flag = 1;
$.each(groups, (function(index, elem) {
group_names.push($(elem).val());
}))
$.each(users, (function(index, elem){
user_names.push($(elem).val());
}))
if (group_names.length > 0 || user_names.length > 0) {
$.ajax({
url: 'auth_role/check_groups_and_users',
dataType: "json",
contentType: "application/json; charset=utf-8",
data: {group_names: group_names, user_names: user_names},
success:function(data) {
$("input[id='auth_groups_name']").css("background-color", "white");
$("input[id='users_']").css("background-color", "white");
if (data.invalid_groups.length > 0) {
submit_flag = 0;
// mark invalid groups
alert("At least one group is not a valid group.");
$.each(data.invalid_groups, function(index, elem) {
var ind = group_names.indexOf(elem);
$("input[id='auth_groups_name']").eq(ind).css("background-color", "#FFB2B2");
//$("input[id='auth_groups_name'][value='nen_project']").parent().parent().append('<td>Not a valid group.</td>');
});
}
if (data.invalid_users.length >0 ) {
submit_flag = 0;
alert("At least one user is not a valid user.");
$.each(data.invalid_users, function(index, elem) {
var ind = user_names.indexOf(elem);
$("input[id='users_']").eq(ind).css("background-color", "#FFB2B2");
});
}
if (submit_flag == 1) {
$("#update_role_form").submit();
}
}
});
} else {
$("#update_role_form").submit();
}
}
function export_perms() {
window.location.href = "<%= export_permissions_admin_path(:format => 'tsv') %>";
};
function export_roles() {
window.location.href = "<%= export_roles_admin_path(:format => 'tsv') %>";
};
</script>
<% end %>
<% content_for :title do %>
Administrator Panel
<% end %>
<h1><%= yield :title %></h1>
<h2>Build Logging</h2>
<%= form_tag("/admin/toggle_logging", :method =>"post", :id => "toggle_logging") do %>
The build detailed logging is currently <%= "#{#system_settings.detailed_logging ? 'Enabled': 'Disabled'}" %>
<br/><br/>
<%= submit_tag "#{#system_settings.detailed_logging ? 'Disable' : 'Enable'} Logging" ,:class=>"submit_auth_button"%>
<% end %>
<hr>
<% row_class = "even"
groups = []
def replaceSpaces(str)
if str.to_s.strip.length == 0
return str
else
copy = str
copy = copy.gsub " ", "_"
copy = copy.gsub "-", "_"
copy = copy.gsub ":", "_"
copy = copy.gsub "__", "_"
copy = copy.downcase
return copy
end
end
def cleanDescription(str)
if str.to_s.strip.length == 0
return str
else
copy = str
copy = copy.gsub "Edit CR Field: ", ""
copy = copy.gsub "Task: ", ""
copy = copy.gsub "Element: ", ""
return copy
end
end
def cleanDependsOn(str)
if str.to_s.strip.length == 0
return str
else
copy = replaceSpaces(str)
copy = copy.gsub "|", " "
return copy
end
end
%>
<h2>Permissions For Roles </h2>
<%= link_to_function raw("#{image_tag('export.png')} Export Permissions as TSV"), "export_perms()" %>
</br>
</br>
<%= form_tag("/auth_permission/update", :method =>"put", :id => "perms_checkbox_form") do %>
<table>
<tr>
<th class='permissionname'> Permission </th>
<% #roles.each do |role| %>
<th class='rolename'><%=role.name %></th>
<% end %>
</tr>
<% #permissions.each do |perm| %>
<%= fields_for "perm_role[]", perm do |perm_fields| %>
<% if !groups.include? perm.group %>
<% groups << perm.group %>
<tr class='group_header'>
<td onclick='toggleGroup("<%="#{replaceSpaces(perm.group)}"%>", this)'><a href='javascript:void(0);' id='toggler'><%=perm.group%><span id='info_helper'> (click to toggle)</span></a></td>
<% #roles.each do |perm_role| %>
<td class='permission_chkbox all_<%=replaceSpaces(perm.group) %> all_<%=replaceSpaces(perm_role.name) %>'> <%= check_box_tag("group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}", "unchecked", false ) %>
<% end %>
</tr>
<% end %>
<tr class='<%= row_class %>'>
<td class='indented_description'><%="#{cleanDescription(perm.description)}" %>
<% if perm.depends_desc %>
<br/>
<span class='depends_description <%="#{cleanDependsOn(perm.depends_on)}" %>'>
<%="#{perm.depends_desc}" %>
</span>
<% end %>
</td>
<% #roles.each do |perm_role| %>
<td class='permission_chkbox <%=replaceSpaces(perm.group) %> <%=replaceSpaces(perm_role.name)%>'> <%= check_box_tag("perm_role[#{perm.name},#{perm_role.name}]", "", perm.authroles.include?(perm_role) ) %></td>
<% end %>
<%
if row_class == 'even'
row_class = 'odd'
else
row_class = 'even'
end
%>
<% end %>
</tr>
<%end%>
</table>
<br/>
<%= submit_tag "Update Permissions" ,:class=>"submit_auth_button"%>
<% end %>
<script>
<% usednames = [] %>
<% jsgroups = [] %>
<% #permissions.each do |perm| %>
<% #roles.each do |perm_role| %>
var elementId = '#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>';
<% if !jsgroups.include? "#{perm_role.name} #{perm.group}" %>
<% jsgroups << "#{perm_role.name} #{perm.group}" %>
$(elementId).click(function(){
// Check all the children
if(this.checked){
$(elementId).prop("checked", true);
//console.log($('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input'))
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function() {
$(this).prop("checked", true)
});
}
else{
$(elementId).prop("checked", false);
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function() {
$(this).prop("checked", false)
});
}
});
<% end %>
// Need to clear out this variable otherwise it'll get confused
elementId = ""
// Check if everything under that group/role is already checked
// If it is then check the Group Header
// If it's not don't do anything
var is_all_checked = true;
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function(){
if(!this.checked || typeof(this.checked) == "undefined") is_all_checked = false
});
if(is_all_checked){
$('#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>').prop("checked", true);
}
// If any of the children permission roles get change, let's figure out if we need to check/uncheck
// the parent
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').change(function(){
// Check if everythin in that gorup is already filled out
var is_all_checked = true;
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function(){
if(!this.checked || typeof(this.checked) == "undefined") is_all_checked = false
});
if(is_all_checked){
$('#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>').prop("checked", true);
}
if(this.checked){
}
else{
// uncheck the parent if any of its children are unchecked
$('#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>').prop("checked", false);
//console.log("uncheck parent");
}
});
<% t = ".#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)} input" %>
<% if !usednames.include? t %>
<% usednames << t %>
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').change(function(){
var rolename = '<%="#{replaceSpaces(perm_role.name)}"%>';
var that = this;
var old_color = $(that).parent().parent().children().first().children('.depends_description').css('background-color')
$(that).parent().parent().children().first().children('.depends_description').animate({backgroundColor:'red', opacity:0.75}, 500);
$(that).parent().parent().children().first().children('.depends_description').animate({backgroundColor:old_color, opacity:1}, 100);
var classes = $(that).parent().parent().children().first().children('.depends_description').attr("class");
if(classes && classes != "") {
classes = classes.split(" ")
for(var k in classes){
if(classes[k] != "depends_description"){
// console.log("toggle", classes[k])
highlightGroup(classes[k], rolename);
}
}
}
});
<% end %>
<% end %>
<% end %>
</script>
<hr/>
<h2> Roles </h2>
<%= link_to_function raw("#{image_tag('export.png')} Export Roles as TSV"), "export_roles()" %>
</br>
</br>
<% if #role %>
<div id="selector_div">
<table>
<tr>
<td> Select Role: </td>
<td>
<%= select_tag "roles_select", options_for_select(#roles.collect {|role_option| [role_option.name, role_option.id]}, :selected => #role.id), :onchange => "render_auth_role_partial()", :class=>"role_selection" %>
</td>
<td>
<%= button_to_function "Create New Role", "show_new_role_form()", :class=>"small_auth_button" %>
</td>
<td>
</td>
</tr>
</table>
</div>
<% end %>
<hr>
<%= render :partial => 'auth_role_form', :locals => {:role => #role, :roles => #roles}%>
admin_controller.rb
class AdminController < ApplicationController
before_action :check_permission
def index
puts "stepped in index"
#system_settings = AuthSettings.first
#permissions = AuthPermission.sort(:group.asc, :name.asc)
#roles = AuthRole.all(:order => :name)
#role = #roles.first
#users = User.administrators
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #users, :status => :ok }
format.json { render :json => #users, :status => :ok }
end
end
def create
usernames = params[:admin][:usernames].split(',')
#users = []
usernames.each do |username|
username.strip!
next if username.empty?
begin
#users << User.find_or_create_admin(username)
rescue
logger.error("Couldn't save user with name: #{username}")
end
end
respond_to do |format|
format.html { redirect_to(:action=>:index) }
format.xml { render :xml => #users, :status=>:created }
format.json { render :json => #users, :status=>:created }
format.js { render :layout=>false }
end
end
def destroy
#user = User.find_by_name(params[:id])
if #user
#user.admin = false
if #user.save
respond_to do |format|
format.html
format.xml { render :xml => #user }
format.js { render :layout => false }
end
end
else
redirect404
end
end
def user_search
username = params.delete(:q)
if username
#users = User.search_for_usernames(username)
respond_to do |format|
format.json { render :json => #users }
end
else
# TODO: this is erroneous because this particular request is always going to be a JSON request via the view
# If the request was made from the outside, and it was an HTML request, it's not going to render anything...
# regardless of whether or not the q parameter was supplied
flash[:error] = "You need to supply a 'q' parameter in order to search"
respond_to do |format|
format.html { render :action=>:index }
end
end
end
def group_search
groupname = params.delete(:q)
if groupname
#groupnames = User.search_for_groups(groupname)
respond_to do |format|
format.json { render :json => #groupnames }
end
else
# TODO: this is erroneous because this particular request is always going to be a JSON request via the view
# If the request was made from the outside, and it was an HTML request, it's not going to render anything...
# regardless of whether or not the q parameter was supplied
flash[:error] = "You need to supply a 'q' parameter in order to search"
respond_to do |format|
format.html { render :action=>:index }
end
end
end
def toggle_logging
Rails.logger.info params
setting = AuthSettings.first
setting.detailed_logging = !setting.detailed_logging
Rails.logger.info "Logging is #{setting.detailed_logging}"
if setting.save
flash[:notice] = "Detailed logging #{setting.detailed_logging ? 'enabled' : 'disabled'}."
else
flash[:error] = "Could not change logging"
end
redirect_to admin_index_path
end
def toggle_privs
current_user.admin_enabled = !current_user.admin_enabled
if current_user.save
flash[:notice] = "Administrative privileges #{current_user.admin_enabled ? 'enabled' : 'disabled'}."
else
flash[:error] = "Could not change your privilege level."
end
redirect_to admin_index_path
end
def export_permissions
respond_to do |format|
format.tsv do
tsv = []
column_headers = [:description, :depends_desc, :authroles]
# Filter records.
records = AuthPermission.sort(:group.asc, :name.asc)
# Render TSV lines.
records.each do |record|
element_line = []
column_headers.each do |column|
begin
if column == :authroles
value_arr = []
value = ""
record.authroles.each do |role|
value_arr << role.name
end unless !record.authroles
value << value_arr.join(",")
else
value = eval("record.#{column}")
end
element_line << value
rescue
element_line << nil
end
end
tsv << element_line.join("\t")
end
# Add column headers to the top of the matrix.
tsv.unshift([column_headers].flatten.join("\t"))
render :text => tsv.join("\r\n")
end
end
end
def export_roles
respond_to do |format|
format.tsv do
tsv = []
column_headers = [:name, :description, :groups, :users]
# Filter records.
records = AuthRole.all(:order => :name)
# Render TSV lines.
records.each do |record|
element_line = []
column_headers.each do |column|
begin
if column == :groups
value_arr = []
value = ""
record.groups.each do |group|
value_arr << group.name
end unless !record.groups
value << value_arr.join(",")
elsif column == :users
value = record.users.join(",")
else
value = eval("record.#{column}")
end
element_line << value
rescue
element_line << nil
end
end
tsv << element_line.join("\t")
end
# Add column headers to the top of the matrix.
tsv.unshift([column_headers].flatten.join("\t"))
render :text => tsv.join("\r\n")
end
end
end
private
def check_permission
ensure_task_permission('task:manage_user_group')
end
end
As per your routes definition, if the URI needed for a GET request is is /admin/user_search then you should be using user_search_admin_index_path - notice the _index suffix.

Ruby on Rails 6 Turbolinks Stripe form values not persisting on back

I am trying to build a form that allows a user to make a booking for a martial arts class they wish to attend. I have created a form that dynamically changes based on the selections the user makes, when I change any of the select options the form updates and when I submit the form it redirects to a Stripe checkout. The problem I have is after submitting the the form and I click the browsers back button or the back button provided on the Stripe checkout page the select options that have been updated have reverted back to the default options rather than the updated options. Can anyone help me correct this behaviour and get the correct form elements to persist?
Here is the code I am using to do this:
The view I am rendering the form in:
<% content_for :banner_title, #page_data['bannerTitle'] %>
<% content_for :head do %>
<meta name="turbolinks-cache-control" content="no-cache">
<% end %>
<div class="content container py-5">
<div class="row">
<div class="col-12 col-md-7 mx-auto">
<%= render "forms/booking", options: #options %>
</div>
</div>
</div>
The form I am using:
<%= form_for #booking, class: 'booking clearfix' do |f| %>
<div class="form-group">
<%= f.label(:class_name, "Select a class you wish to attend: ") %>
<%= f.select(:class_name, options_for_select(options[:class_names], #booking.class_name), {}, class: 'form-control' ) %>
</div>
<div class="form-group">
<%= f.label(:date, "Select a date:") %>
<%= f.select(:date, options_for_select( options[:dates], #booking.date ), {}, class: 'form-control' ) %>
</div>
<div class="form-group">
<%= f.label(:time, "Select a time: ")%>
<%= f.select(:time, options_for_select(options[:times], #booking.time), {}, class: 'form-control') %>
</div>
<div class="form-group">
<%= f.label(:attendees, "How many attending: ") %>
<%= f.select(:attendees, options_for_select(options[:attendees], #booking.attendees), {}, class: 'form-control' )%>
</div>
<%= f.submit 'Place Booking', class: 'btn btn-primary btn-lg text-light float-right', id: 'create-booking' %>
<% end %>
<%= javascript_pack_tag 'booking_form' %>
<script src="https://js.stripe.com/v3/"></script>
The model for the form (I'm not using ActiveRecord, i dont know if this makes any difference?):
class Booking
include ActiveModel::Model
MAX_ATTENDEES = 10
attr_accessor :time, :class_data, :attendees, :date, :class_name
def initialize(args={})
#time = args['time']
#class_name = args['class_name']
#class_data = args['class_data']
#date = args['date']
#attendees = args['attendees']
end
def day
#date.split(',').first
end
def available_dates
days_index_array = class_data['times'].keys.map {|k| day_index(k) }
days_within( days_index_array )
end
def available_times
if !date
class_data['times'][class_data['times'].keys.first.downcase]
else
class_data['times'][day.downcase]
end
end
def total_cost
#class_data['cost'].to_i * #attendees.to_i
end
def attending_string
ActionController::Base.helpers.pluralize(attendees, 'person')
end
private
def days_within(days, timeframe=1.month)
start_date = Date.tomorrow
end_date = start_date + timeframe
(start_date..end_date).to_a.select {|k| days.include?(k.wday) }
end
def day_index(day)
DateTime::DAYNAMES.index(day.to_s.capitalize)
end
end
And the controller I am calling the new action in:
class BookingsController < ApplicationController
include BookingsHelper
before_action :set_class_data
skip_before_action :set_page_data, except: :new
def new
set_booking
# store values to be passed to the form helper method options_for_select. Each value must be an array populated with arrays with the format [value, text]
#options = {
class_names: #class_data.map {|c| [ c['name'], c['name'] ]},
dates: #booking.available_dates.map {|d| [d.strftime('%A, %d %B'), d.strftime('%A, %d %B')] },
times: #booking.available_times.map {|t| [t,t]},
attendees: Booking::MAX_ATTENDEES.times.map {|i| [i+1, i+1]}
}
end
def create
end
def booking_form_data
booking_form_data = set_booking_form_data(params)
update_session_booking(booking_form_data)
render json: booking_form_data
end
private
def set_booking
if session[:current_booking]
pp "session exists"
#booking = Booking.new(session[:current_booking])
else
pp "session does not exist"
#booking = Booking.new
session[:current_booking] = #booking.instance_values
end
set_booking_class_data
end
def set_booking_class_data
!#booking.class_name ? #booking.class_data = #class_data.first.except('information') : #booking.class_data = #class_data.find {|cd| cd['name'] == #booking.class_name}.except('information')
end
def booking_params
params.permit(:class_name, :date, :time, :attendees, :update_type)
end
def update_session_booking(booking_form_data)
if params[:update_type] == 'class_name'
session[:current_booking]['class_name'] = params[:class_name]
session[:current_booking]['date'] = booking_form_data[:date_options].first
session[:current_booking]['time'] = booking_form_data[:time_options].first
elsif params[:update_type] == 'date'
session[:current_booking]['date'] = params[:date]
session[:current_booking]['time'] = booking_form_data[:time_options].first
elsif params[:update_type] == 'time'
session[:current_booking]['time'] = params['time']
elsif params[:update_type] == 'attendees'
session[:current_booking]['attendees'] = params[:attendees]
elsif params[:update_type] == 'load'
session[:current_booking] = booking_params.except(:update_type)
end
pp "Session Booking: #{session[:current_booking]}"
end
def set_booking_form_data(params)
booking_form_data = {}
selected_class = #class_data.find {|cd| cd['name'] == params[:class_name] }
# when the class_name select is changed
if params[:update_type] == 'class_name'
booking_form_data[:date_options] = days_within( selected_class['times'].keys.map {|k| day_index(k) } ).map {|d| d.strftime('%A, %d %B') }
booking_form_data[:time_options] = selected_class['times'][booking_form_data[:date_options].first.split(',')[0].downcase]
# when date select is changed
elsif params[:update_type] == 'date'
booking_form_data[:time_options] = selected_class['times'][params[:date].split(',')[0].downcase]
end
booking_form_data
end
end
And the javascript I am using to update the form:
getBookingFormData = (bodyData={}, successCallback=()=>{}) => {
$.ajax({
url: '/booking_form_data',
method: 'POST',
beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
data: bodyData,
success: successCallback
})
}
createOptions = (values) => {
let newOptions = [];
$.each(values, (index, value) => {
let newOption = $('<option></option>');
newOption.attr('value', value);
newOption.text(value);
newOptions.push(newOption)
})
return newOptions
}
appendOptions = (options, element) => {
$(element).empty();
$(element).append(options)
}
currentFormValues = () => {
return {
class_name: $('#booking_class_name').val(),
date: $('#booking_date').val(),
time: $('#booking_time').val(),
attendees: $('#booking_attendees').val()
}
}
$('select#booking_class_name').on('change', () => {
let bodyData = {
class_name: $('select#booking_class_name').val(),
update_type: 'class_name'
}
let successCallback = (res) => {
let dateOptions = createOptions(res.date_options);
let dateSelect = $('select#booking_date');
let timeOptions = createOptions(res.time_options);
let timeSelect = $('select#booking_time');
appendOptions(dateOptions, dateSelect);
appendOptions(timeOptions, timeSelect);
}
getBookingFormData(bodyData, successCallback)
});
$('select#booking_date').on('change', () => {
let bodyData = {
class_name: $('select#booking_class_name').val(),
date: $('select#booking_date').val(),
update_type: 'date'
};
let successCallback = (res) => {
let timeOptions = createOptions(res.time_options);
let timeSelect = $('select#booking_time');
appendOptions(timeOptions, timeSelect);
}
getBookingFormData(bodyData, successCallback)
});
$('select#booking_time').on('change', () => {
let bodyData = {
time: $('select#booking_time').val(),
update_type: 'time'
};
getBookingFormData(bodyData);
});
$('select#booking_attendees').on('change', () => {
let bodyData = {
attendees: $('select#booking_attendees').val(),
update_type: 'attendees'
};
getBookingFormData(bodyData);
});
$('#create-booking').on('click',(e) => {
e.preventDefault();
bookingDefault = false
const stripe = Stripe(process.env.STRIPE_PUBLIC);
let requestHeaders = new Headers({
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
'Content-Type': 'application/json'
})
fetch('/create_checkout_session', {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(currentFormValues())
})
.then((res) => { return res.json() })
.then((session) => { return stripe.redirectToCheckout({ sessionId: session.id }) })
.then((result) => {
if (result.error) { alert(result.error.message) }
})
.catch((error) => { console.error('Error: ', error) })
})
From what Ive read i think it may be a problem related to caching which makes me think this is an issue with turbolinks but I could be completely wrong. Ive tried adding meta tags that disable turbolinks or force it to reload the page but they did not seem to work.
Any input at all would be really appreciated as Ive been stuck on this for days. Let me know if you need any more information
This isn't so much Stripe-related as it is related you your form value management. If you want to keep these values around, you'll need to build that into your front application, somehow. There are lots of options for this:
Using local storage
Using query parameters, if not sensitive info
Using a cookie and a server session you can re-retrieve and hydrate the f.select options with a default value.

Filter select fields based on previous select fields values

I'm new at RoR and I'm having a trouble in my app. The problem consists on filter a select field named "Solution", based on the others select fields above it.
Now, what the app do is to retrieve all information from BD about Area, Region, Associated, Solution and populate the select fields with these data. But the user wants that, when an area, a region and an associated is selected by the user, only the solutions about that associated in that region on that area should be shown.
Edit:
I'm almost there! I've made many changes in my app. The select fields are populated by controller action new and the function "populate_selects", which is called by the parameter before_action :popula_selects, only: [:new, :edit]. A new function was created in order to be called by AJAX and upgrade the "Solution" field:
Atendments_Controller < ApplicationController
before_action :populate_selects, only: [:new, :edit]
def new
#atend = atendment.new
end
def update_solution #AJAX
#solutions = atendment.joins(:solution).where("atendment_area_id = ? and atendment_region_id = ? and atendment_assoc_id = ?", params[:atendment_area_id], params[:atendment_region_id], params[:atendment_assoc_id])
respond_to do |format|
format.js
end
end
private
def populate_selects
#atendment_area = atendmentArea.where(status: true, user_id: current_user.id)
#atendment_region = atendmentRegion.where(status: true, user_id: current_user.id)
#atendment_assoc = atendmentRegionAssoc.where(status: true, assoc_id: current_user.entidade_id).where(atendment_region_id: #atendment_region.map(&:atendment_region_id))
#solutions = atendment.joins(:solution).where("atendment_area_id = ? and atendment_region_id = ? and atendment_assoc_id = ?", params[:atendment_area_id], params[:atendment_region_id], params[:atendment_region_assoc_id])
end
end
Below, the _form.html.erb code from view:
<div class="atendment-form">
<%= form_for :atendment, url: {action: "new"}, html: {method: "get"} do |f| %>
<div class="col-xs-6">
<%= f.select :atendment_area_id, options_for_select(#atendment_area.collect { |c| [ c.atendment_area.name, c.id ] }, 1), {:prompt=>"Área"}, { :class => 'form-control', :required => true, id: 'atendment_atendment_area_id' } %>
</div>
<div class="col-xs-6">
<%= f.select :atendment_region_id, options_for_select(#atendment_region.collect { |c| [ c.atendment_region.name, c.id ] }, 1), {:prompt=>"Região"}, { :class => 'form-control', :required => true, id: 'atendment_atendment_region_id' } %>
</div>
</div>
</div>
<div class="field">
<%= f.select :atendment_assoc_id, options_for_select(#atendment_assoc.collect { |c| [ c.atendment_region.name, c.id ] }, 1), {:prompt=>"Associado"}, { :class => 'form-control', :required => true, id: 'atendment_atendment_assoc_id' } %>
</div>
<div class="field">
<%= f.select :solution_id, options_for_select(#solutions.collect { |solution| [solution.name, solution.id] }, 0), {:prompt=>"Solução"}, { :class => 'form-control', :required => true, id: 'atendment_solution_id' } %>
</div>
</div>
Route to the new function:
resources :atendments do
collection do
get :update_solution
end
end
AJAX function which calls the "update_solution" and reset solution field's value (app/assets/javascript/atendment.js.coffee):
show_solutions = ->
$.ajax 'update_solution',
type: 'GET'
dataType: 'script'
data: {
atendment_area_id: $("#atendment_atendment_area_id").val()
atendment_region_id: $("#atendment_atendment_region_id").val()
atendment_assoc_id: $("#atendment_atendment_assoc_id").val()
}
error: (jqXHR, textStatus, errorThrown) ->
console.log("AJAX Error: #{textStatus}")
success: (data, textStatus, jqXHR) ->
console.log("OK!")
$(document).ready ->
$('#atendment_atendment_assoc_id').on 'change', ->
show_solutions()
So, I've created a .coffee file to render the partial that will return a new value to the "solution" field "option" tag
(app/views/atendment/update_solution.coffee):
$("#atendment_solution_id").empty()
.append("<%= escape_javascript(render :partial => 'solution') %>")
And, the last but not least, the partial containing the html code for the "option" tag mentioned above (app/views/atendments/_solution.html.erb):
<option value="<%= solution.id %>" selected="selected"><%= solution.nome %></option>
For any reason, the AJAX function doesn't print nothing on console (nor error neither success), but it calls the update_solution.coffee file. The point is, it doesn't update the option value due an error (500 internal server error). I don't know what am I doing wrong. If anybody could help me, I appreciate it.
I would do this with JS, can think any other way.
A function called by onchange that change the display attribute from each field that you need to hide or show.
I solved this with the following code:
assets/js/atendments.js
I changed the code because the last one had many bugs.
function getAssociated(){
var aau_id = $("#atendment_area_user_id").val()
var aru_id = $("#atendment_region_user_id").val();
$.getJSON("/controllers/atendments_controller/getAssociated/"+aru_id,
function ( callback ) {
if (callback != "error"){
var assoc = document.getElementById("atendment_region_associated_id");
while (assoc.firstChild) {
assoc.removeChild(assoc.firstChild);
}
var i = Object.keys(callback).length -1;
$("#atendment_region_associated_id").append("<option value=''>Associated</option>");
while (i >= 0) {
$("#atendment_region_associated_id").append("<option value='"+callback[Object.keys(callback)[i]]+"'>"+Object.keys(callback)[i]+"</option>");
i--;
}
}
});
get_solution_type();
}
function get_solution_type() {
var ara_id = $("#atendment_region_associated_id").val();
$.getJSON("/controllers/atendments_controller/getSolution/"+ara_id,
function ( callback ) {
if (callback != "error"){
var sol = document.getElementById("atendment_solution_id");
while (sol.firstChild) {
sol.removeChild(sol.firstChild);
}
var i = Object.keys(callback).length-1;
while (i >= 0) {
$("#atendment_solution_id").append("<option value='"+callback[Object.keys(callback)[i]]+"'>"+Object.keys(callback)[i]+"</option>");
i--;
}
}
});
var aau_id = $("#atendment_area_user_id").val();
$.getJSON("/controllers/atendments_controller/getType/"+aau_id,
function ( callback ) {
if (callback != "erro"){
var type = document.getElementById("atendment_type_id");
while (type.firstChild) {
type.removeChild(type.firstChild);
}
var i = 0;
while (i < (Object.keys(callback).length)) {
$("#atendment_type_id").append("<option value='"+callback[Object.keys(callback)[i]]+"'>"+Object.keys(callback)[i]+"</option>");
i++;
}
}
});
}
The $.getJSON performs ajax request to the controller that responds with JSON and update the select fields option tags.
controllers/atendments_controller
I just retrieve the data from DB and return as JSON
def getAssociated
aru_id = params[:atendment_region_user_id]
aras = AtendmentRegionAssociated.where("SQL here")
if aras.present?
render :json => aras.to_json
else
render :json => "error".to_json
end
end
def getSolution
ara_id = params[:atendment_region_associated_id]
sol = Solution.where("SQL here")
if sol.present?
render :json => sol.to_json
else
render :json => "error".to_json
end
end
def getType
aau_id = params[:atendment_area_user_id]
type = AtendmentType.where("SQL here")
if type.present?
render :json => type.to_json
else
render :json => "error".to_json
end
end
Update the routes and put the javascript functions in select fields onchange property. Now everything is working fine :D

Ajax call on a <select> in a formtastic form

I need in my app to select from a list of provider a provider, and then, via ajax, I could see below a list of categories, that belongs to a specific provider. I have a form in activeadmin:
<%= semantic_form_for [:admin, #game], builder: ActiveAdmin::FormBuilder do |f| %>
<%= f.semantic_errors :state %>
<%= f.inputs do %>
<%= f.input :categorization_id, label: 'Provider', as: :select,
collection: Provider.all.map { |provider| ["#{provider.name}", provider.id] },
input_html: { class: (:provider_select), 'data-url': category_select_path(provider: 4) } %>
<%= f.input :categorization_id, label: 'Category',input_html: { class: ('category_dropdown') }, as: :select,
collection: Category.all.map { |category| ["#{category.name}", category.id]}%>
...
<% end %>
<%= f.actions %>
<% end %>
In activeadmin controller I have:
controller do
def ajax_call
#provider = Provider.find(params[:provider])
#categories = #provider.categories
respond_to do |format|
format.json { render json: #categories }
end
end
end
JS:
$(document).on('ready page:load', function () {
$('.select.input.optional').last().addClass('hidden_row');
$('#game_categorization_id').change(function () {
var id_value = this.value;
$('.hidden_row').removeClass('hidden_row');
$.ajax({
type: 'GET',
url: '/admin/games/category_select'
// data: id_value
})
});
});
And the routes: match '/admin/games/category_select' => 'admin/games#ajax_call', via: :get, as: 'category_select'
I don't have an idea, how can I pass providers id from collection into url. Currently, I have there category_select_path(provider: 4), but actually it has to be smth. like this - category_select_path(provider: provider.id) In the browser, in a Network tab of devtools I can see my category_select, but there is an error: Couldn't find Game with 'id'=category_select. I can't figure out from where does it come. Any suggestions? Thanks.
The problem was in routes: match '/admin/games/category_select' => 'admin/games#ajax_call', via: :get, as: 'category_select'. It is reserved by a show action of Activeadmin controller. So I changed my routes to: get '/admin/select_category' => 'admin/games#get_providers_categories', as: 'select_category', and added to ajax call data: {provider: provider}, so I could fire in params id of provider:
$.ajax({
type: 'GET',
url: '/admin/select_category',
data: {
provider: provider
},
success: (function (data) {
$('#select_category').children('option').remove();
$('#select_category').prepend('<option value=""></option>');
$.each(data.categories, function () {
$('#select_category').append('<option value="' + this.id + '">' + this.name + '</option>')
})
})
})

acts_as_list problem with scope

The problem I have is at the bottom.
Models:
class Skill
has_many :tags
acts_as_list :column => 'sequence'
end
class Tag
belongs_to :skill
acts_as_list :column => 'sequence', :scope => :skill
end
View:
<table id="skills">
<% #skills.each do |s| %>
<tr id="skill_<%= s.id %>">
<td>
<%= s.name %>
</td>
<td>
<ul id="tags">
<% s.tags.each do |t| %>
<li id="tag_<%= t.id %>">
<%= t.name %>
</li>
<% end %>
</ul>
</td>
</tr>
<% end %>
</table>
jQuery for drag and drop:
$( "#skills" ).sortable({
axis: 'y',
dropOnEmpty: false,
handle: '.handle',
cursor: 'move',
items: 'tr',
opacity: 0.4,
scroll: true,
update: function(){
$.ajax({
type: 'post',
data: $('#skills').sortable('serialize') + "&authenticity_token=" + "<%= form_authenticity_token %>",
dataType: 'script',
complete: function(request){
$('#skills').effect('highlight');
},
url: '<%= url_for :action => 'sort', :controller => 'skills' %>'
})
}
});
$( "#tags" ).sortable({
axis: 'y',
dropOnEmpty: false,
handle: '.handle',
cursor: 'move',
items: 'li',
opacity: 0.4,
scroll: true,
update: function(){
$.ajax({
type: 'post',
data: $('#tags').sortable('serialize') + "&authenticity_token=" + "<%= form_authenticity_token %>",
dataType: 'script',
complete: function(request){
$('#tags').effect('highlight');
},
url: '<%= url_for :action => 'sort', :controller => 'tags' %>'
})
}
});
Controller for Tags:
def sort
#tags = Tag.all
#tags.each do |tag|
tag.sequence = params['tag'].index(tag.id.to_s) + 1
tag.save
end
render :nothing => true
end
PROBLEM:
After dragging the tags there is an error:
NoMethodError (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.+):
app/controllers/tags_controller.rb:12:in `block in sort'
app/controllers/tags_controller.rb:11:in `each'
app/controllers/tags_controller.rb:11:in `sort'
I found the error is gone if I load tags belonging to a specific Skill like this:
def sort
#tags = Skill.find(1).tags
Question - how to tell the controller which tags to load (but not all tags)?
A SOLUTION I FOUND...
Tags controller:
def sort
#tag = Tag.find(params[:tag]).first
#skill = Skill.find_by_id(#tag.skill_id)
#tags = #skill.tags
Is this the best way to do it?
The error occurred while evaluating nil.+):
This means that whatever params['tag'].index(tag.id.to_s) + 1 (from sort in controller) is supposed to do is actually resulting in nil + 1.
If your solution performs as expected, then I don't see a problem with it.
As a tip, #skill = Skill.find_by_id(#tag.skill_id) could be shortened to #skill = #tag.skill if you do belongs_to :skill in your Tag model.

Resources