Identifying a item in one of multiple ways - ruby-on-rails

I'm designing a form where you need to add a relation to another object. Okay that's normally fine, but what I am hung up on is a clean way to make it easy for the user to enter the object. There are multiple ways that the user could know how to specify the object (unique identifiers).
Let's take the example of associating a user to a task. In this case, the models are laid out like this:
class User
has_many :tasks
# fields: phone_number, email, username
validates_uniqueness_of :phone_number
validates_uniqueness_of :email
validates_uniqueness_of :username
# other methods, validations, etc which are not important.
end
class Task
belongs_to :user
# other methods, validations, etc which are not important.
end
How would I write the controller and view form, if I want to be able to specify the user by username, email, or phone_number - I might know any one of these, and just one is enough to specify exactly the user that I want, in a clean way?
Currently my solution seems messy. I have a view similar to:
<% form_for #task do |f| %>
... Other stuff
User - choose one of the following ways: <br />
Username: <%= text_field_tag :user_name %> <br />
or phone number: <%= text_field_tag :user_phone %> <br />
or email: <%= text_field_tag :user_email %> <br />
... More other stuff
<% end %>
I then handle these fields explicitly in the controller, finding the actual user based on which ones are filled in:
class TasksController
def create
#task = Task.new(params[:task])
if params[:user_name]
#task.user = User.find_by_username(params[:user_name])
elsif params[:user_phone]
#task.user = User.find_by_phone_number(params[:user_phone])
elsif params[:user_email]
#task.user = User.find_by_email(params[:user_email])
end
if #task.save
redirect_to #task
else
render :action => 'new'
end
end
end
This seems like it's very specific, and there is a lot of code in my controllers, especially if I have lots of these on a form. Don't even talk to me about when you need to dynamically add multiple users to a task - it gets even more crazy in the controller parsing everything out.

I think the most unobtrusive way of doing this is with a few AJAX calls.
I'm thinking an observer on a text field that calls a remote function. That function should update your form with matching potential associations, that the user can select with a radio button or something.
It would look like something like this:
UserController:
def select_user
#user = case params[:query]
when !/\w/ #phone no
User.find_by_phone(params[:query])
when /#/ # email
User.find_by_email(params[:query])
else
User.find_by_username(params[:query])
end
end
views/select_user.rjs:
page.replace_html :matched_user, :inline => <<"PARTIAL"
<%=hidden_field_tag "task[user_id]", #user.id%>
User: <%=#user%>
PARTIAL
task form
<%form_for #task do |f|%>
...
<%= text_field_tag :query %>
<%= observe_field :query, {:controller => :users, :action => :select_user}%>
<div id="matched_user" />
<% end%>
May not work as advertised. I made assumptions and haven't tested it. But it should put you on the right track.
When it comes to multiple users all that really needs to change is the hidden field tag and the rjs file. but not by much. There's also nothing stopping you from using a more robust search mechanism.

Phone numbers are going to be a problem, unless you process the input to strip out hypens, parentheses, etc.
Having said that, here are a couple of options.
1 Roll your own.
In User:
def find_by_foo(search)
# TODO: Pre-process anything in search that looks like a phone number.
conditions = "username like ? or phone_number like ? or email like ?"
wildcard_search = "%#{search}%"
substitutions = []
3.times do
substitutions << wildcard_search
end
User.find(:first, :conditions => ([conditions] + substitutions))
end
2 Rolling your own is stupid when searchlogic already exists.
From their examples under "combining scopes":
User.username_or_first_name_like("ben")
=> "username LIKE '%ben%' OR first_name like'%ben%'"

Related

How to create new text field

I know this is probably a very basic question but I am brand new to Ruby and kinda in a dead end. I have made a simple little site with profiles and profile pages. But on the profile pages I would like to add a new text field like "Bio" for instance where the user types in a bio about himself and it shows. Im just at a blank on how to create a new text field where people can input this stuff. I know this is basic stuff just stuck and looking for some help or guidance to a tutorial or something. Thank you in advance
Here's an example copied from another answer:
<%= form_for(:ad, :url => {:action => 'create'}) do |f| %>
<%= f.text_field(:name) %>
<%= f.text_area(:text, "", :size => "50x10") %>
<%= submit_tag("Submit") %>
<% end %>
This is kind of a complicated question, once you think about it, because there are so many parts.
Ruby on Rails is built on a architecture, called Model View Controller or MVC. The three parts together make the user interface that is presented to the user.
Models are the actual data, like the User objects, in this case. To create the model, type in this command to the console:
rails g model User bio:text name:string
This will make a basic user model, which only contains two columns, a column for the bio, and a column for their name. Note that this is very uncomplicated, and this can be expanded on a lot, but for now it will do.
Or, if you already have a user model, type in this command to the console:
rails g migration add_bio_to_users bio:text
Next are the controllers, controllers are, in a way, what connect the models and the views, so they manage all of the logic in the back end, like creating new users, or adding bios to their profiles.
You can create the user controller like this (if you do not already have one):
rails g controller Users new
And then, you can add this code to the new file generated, to add the functionality of adding bios (and showing them, too) (and updating other columns as well):
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
render #user
else
render #user # Handle error accordingly
end
end
def show
#user = User.find(params[:user])
end
private
def user_params
params.require(:user).permit(:name, :bio)
end
Now, to the final part, which is the view, which is the actual thing that is presented to the user:
<%= form_for(:user, :url => {:action => 'update'}) do |f| %>
<%= f.text_field(:name) %>
<%= f.text_area(:bio, "", :size => "50x50") %>
<%= f.submit yield(:button_text) %>
<% end %>
Note that this is just a simple view that assumes that you also have a column name in your User model, you can change this accordingly.
And, finally, to show the user, add this to the show view:
<%= #user.bio %>
to show the bio in the show view.
Good luck!

excluding a value from a form drop down in rails

I have a form drop down, that shows me all the emails in my User db table.
<%= f.collection_select(:accessor_id, User.all,:email ,:email) %>
I want to exclude from this list the value of the current's user email, which I can find with with current_user.email (already defined and working)
I know I can achieve this via the following query:
<%= f.collection_select(:accessor_id, User.select(:email).where("email !=?" , current_user.email),:email ,:email) %>
i wanted to know if it is possible to do this after User.all returned all of the values.
you mean something like
User.all.reject {|user| user == current_user}
or more precisely i would fetch all users somewhere in the controller
def index
#users = User.all
end
and use something like that in the form
<%= f.collection_select(:accessor_id, #users.reject {|user| user == current_user}.map(&:email)) %>
#phoet answer is correct, personally I would probably do this at database level anyhow, something along lines of
class User < ActiveRecord::Base
# ...
def self.all_without(excluded)
where("id NOT IN (?)", excluded)
end
end
<%= f.collection_select(:accessor_id, User.all_without([current_user]), :email ,:email) %>
try to keep the view 'clean' of the details, if possible
# and if you really do only want to pull email from the database, you can chain the query
User.all_without([current_user]).select(:email)

How to create multiple "has_many through" associations through one form?

I'm building a martial arts related database, currently I have the following associations set up:
Student has_and_belongs_to_many :styles
Style has_many :ranks
Student has_many :ranks, through: :gradings (and vice versa)
I'm generating a form as follows, depending on the student's styles:
So the headings are generated by the Style model (Tai Chi, Karate...), then their rankings listed below (taken from the Rank model), and the "Dojo" and "Date" fields should belong to the Grading model once created.
The question: I know how to build a form that creates one association (or one association + its children), but how do I build a form that creates multiple associations at once?
Also, what would be a clean way to implement the following:
Only lines which are ticked become associations
Dojo and date must be filled in for ticked lines to save successfully
If a line is unticked it will destroy any previously created associations
This is what I've currently implemented to retrieve the correct records:
class GradingsController < ApplicationController
before_filter :authenticate_sensei!
def index
#student = Student.includes(:styles).find(params[:student_id])
#ranks = Rank.for_student_styles(#student)
split_ranks_by_style
end
private
def split_ranks_by_style
#karate = #ranks.select_style("Karate")
#tai_chi = #ranks.select_style("Tai Chi")
#weaponry = #ranks.select_style("Weaponry")
end
end
# Rank model
def self.for_student_styles(student)
includes(:style).where("styles.id in (?)", student.styles.map(&:id))
end
def self.select_style(style)
all.map { |r| r if r.style.name == style }.compact
end
Complicated forms like this are best handled in a service object initiated in the primary resource's create or update action. This allows you to easily find where the logic is happening afterwards. In this case it looks like you can kick off your service object in your GradingsController. I also prefer formatting a lot of the data in the markup, to make the handling easier in the service object. This can be done a'la rails, by passing a name like "grade[style]" and "grade[rank]". This will format your params coming in as a convenient hash: {grade: {style: "karate", rank: "3"}}. That hash can be passed to your service object to be parsed through.
Without really grasping the full extent of your specific requirements, let's put together an example form:
<%= form_for :grading, url: gradings_path do |f| %>
<h1><%= #rank.name %></h1>
<%- #grades.each do |grade| %>
<div>
<%= hidden_field_tag "grade[#{grade.id}][id]", grade.id %>
<%= check_box_tag "grade[#{grade.id}][active]" %>
...
<%= text_field_tag "grade[#{grade.id}][date]" %>
</div>
<%- end %>
<%= submit_tag %>
<%- end %>
With a form like this, you get your params coming into the controller looking something like this:
"grade"=>{
"1"=>{"id"=>"1", "active"=>"1", "date"=>"2013-06-21"},
"3"=>{"id"=>"3", "date"=>"2013-07-01"}
}
Nicely formatted for us to hand off to our service object. Keeping our controller nice and clean:
class GradingsController < ApplicationController
def index
# ...
end
def create
builder = GradeBuilder.new(current_user, params['grade'])
if builder.run
redirect_to gradings_path
else
flash[:error] = 'Something went wrong!' # maybe even builder.error_message
render :action => :index
end
end
end
So now we just need to put any custom logic into our builder, I'd probably recommend just making a simple ruby class in your /lib directory. It could look something like this:
class GradeBuilder
attr_reader :data, :user
def self.initialize(user, params={})
#user = user
#data = params.values.select{|param| param['active'].present? }
end
def run
grades = data.each{|entry| build_grade(entry)}
return false if grades.empty?
end
private
def build_grade(entry)
grade = Grade.find(entry['id'])
rank = grade.rankings.create(student_id: user, date: entry['date'])
end
end
There will obviously need a lot more work to pass all the specific data you need from the form, and extra logic in the GradeBuilder to handle edge cases, but this will give you a framework to handle this problem in a maintainable and extensible way.

Rails: Restoring contents of non-model form that uses form_tag

I'm making good progress with my first Rails app (using Rails 3). The MVC interaction is all going fine, but I'm having difficulty with a form that doesn't relate directly to a model.
I'm using form_tag, and in the case of success, everything behaves fine. However, the handling of errors is somewhat unfriendly. The actual error message is stored in the flash and displayed fine by layouts/application.html, but I'd really like it if the form could remember the contents that the user had just filled in. But it doesn't: all the fields reset to their default values.
I love the way that forms for RESTful actions on objects automatically remember their submitted values, and get highlighted in red if there are errors. I'm fine without the red highlight, but I'd really like it if I could make the form's fields keep the submitted values.
Can anyone advise how to do this?
Excerpts from the relevant files:
views/cardsets/import.html.erb:
<%= form_tag :action => :import_data, :id => #cardset do %>
...
<%= text_field_tag "separator", "", :maxlength => 1 %>
...
<%= text_field_tag "formatting_line" %>
...etc (more fields)
controllers/cardsets_controller.rb:
# POST /cardsets/1/import_data
def import_data
success, message = #cardset.import_data(params, current_user)
if success
redirect_to(#cardset, :notice => message)
else
flash.now[:error] = message
render :import
end
end
The second arg to text_field_tag is the value to fill in the field with. Try something like this:
<%= text_field_tag "separator", params[:separator], :maxlength => 1 %>
If your field has a default, you will want to set it from the "show" action for the form:
# GET
def show_form
params[:key] = 'default'
end
# POST
def validate_form_and_act
# Don't set it here to reuse what the user passed.
end
or directly on the template (less good because uses an || every time and adds more controller data to view):
<%= text_field_tag 'name', params[:key] || 'default' %>

collection.build of nested attribute through a remote_link does not create params when submitting form

I have the following model:
class Activity < ActiveRecord::Base
has_many :clientships, :dependent => :destroy, :after_add => :default_client_info
accepts_nested_attributes_for :clientships, :allow_destroy => true
end
In my controller, if I perform the following
def new
#activity = IndividualActivity.new(params[:activity])
#activity.clientships.build(:client => Client.first)
...
end
and then save the form, it creates the relevant params and submits successfully.
However, if I chose to call the following through a remote link
#activity.clientships.build(:client => Client.last)
the view is updated with the new clientship record but when I submit the form, the params[:activity] is not created for the second nested attribute. (Why not!?)
This is the view:
%h1 Create a new Activity
- form_for #activity do |f|
%div
= render "activities/client_selector", :f => f
%div
= f.submit "Save!"
Here is the remote_link's controller action
def add_client
#activity = IndividualActivity.new(session[:individual_activity])
# Refresh client
#activity.clientships.build(:client => Client.find(params[:client_id]))
respond_to do |format|
format.js
end
end
This is the add_client.html.js:
page.replace_html "selected_clients", :partial => 'activities/clients'
This is the activities/clients partial:
- form_for #activity do |f|
- f.fields_for :clientships do |client_f|
%tr
%td= client_f.hidden_field :client_id
%td= client_f.object.client.full_name
Does anyone know how I can troubleshoot this further? I seem to have come to a dead-end with my debugging... One thing to note, there is a double use of the following form_for used in new.html.haml and the activities/clients partial (is this problematic?)
- form_for #activity do |f|
I am on rails v2.3.5
Thanks
You ask about debugging, so the first step may be looking at the server log (log/development.log).
There you should see the "params" hash.
Maybe your params contain "activity"=>{"client_id"=>..} instead of "client_id"=>.. ?
Also look at the generated HTML page - use a Firebug or just use a "view source" method of your browser. Look, especially, for input names.
If everything looks OK, put a few debug calls in your action, and look at the development.log for some database activity - do the SQL queries look like they are doing what you want?
In your question there is no 'save' method. The 'build' method does NOT save the created record. Maybe this is your problem?
def add_client
logger.debug "Creating Activity"
#activity = IndividualActivity.new(session[:individual_activity])
logger.debug "Building clientship"
# Refresh client
#activity.clientships.build(:client => Client.find(params[:client_id]))
logger.debug "#activity = #{#activity.inspect}"
# Maybe you were missing this part of code?
logger.debug "Saving #activity"
#activity.save! # use a ! to easily see any problems with saving.
# Remove in production and add a proper if
logger.debug "Saved. #activity = #{#activity.inspect}"
respond_to do |format|
format.js
end
end
You should create a functional test (in case you haven't already) and ensure that if you send proper parameters, your action works as intended.
The test will narrow your search. If the test fails, you know you have a problem in the action. If the test is OK, you need to ensure the parameters are sent properly, and you probably have the problem in your view.
UPDATE:
You said you have TWO forms on the page. This may be the problem, since only one form may be sent at a time. Otherwise it would need to work in a way which can send two requests in one request.
First thing (useful in all similar problems): validate whether your page has correct HTML structure - for example http://validator.w3.org would be a good start. Try to make the code validate. I know that some people treat a "green" status as a unachievable mastery, but just it's really not so hard. With valid code you may be sure that the browser really understands what you mean.
Second: Place all your inputs in a single form. You have problems with nested attributes. For start, try to manually insert inputs with name like <input name="activity[clientship_attributes][0][name]" value="John"/>, and for existing clientships ensure that there is an input with name = activity[clientship_attributes][0][id].
This is the way nested attributes are handled.
Your view may create such fields automagically. This construction should be what you need: (it worked in one of my old project in rails 2.x, I have just replaced the names with ones you use)
<% form_for(#activity) do |f| %>
<p><%= f.text_field :activity_something %></p>
<% #activity.clientships.each do |clientship| %>
<% f.fields_for :clientships, clientship do |cform| %>
<p><%= cform.text_field :name %></p>
<p><%= cform.text_fiels :something %></p>
<% end %>
<% end %>
<% end %>
If you really want to use a partial there, don't create a new form in the partial. Use only the parts of above code.
To pass a variable to the partial, use :locals attribute in the place where you call render :partial:
<%= render :partial => 'clientship', :locals => {:form => f} %>
Then, in your partial, you may use a local variable form where you would use f outside of the partial. You may, of course, map the variables to the same name: :locals => {:f => f}

Resources