In my Rails 3 app, I want to allow a user to specify which parts of their profile can be searchable by others. I know how to do this if I wanted to make the entire user invisible, but how can I set it up so multiple fields can be designated searchable or not separately?
More info:
In terms of functionality, I want to limit searches based on what parts of their profile a user chooses to be searchable in /settings. Parts of the profile would be, for example, #user.profile.hometown or #user.profile.current_city. I'm working off of a Combination of Railscasts #52 and Trevor Turk's tutorial to set what others can search through checkboxes in the settings.
When searchability is defined in settings, when a user searches (or filters) the /users index, what isn't hidden will be public and searchable. In terms of how this works in the DB as far as hiding table columns or grouping, I thought about hiding tables but maybe that's not the best solution. I'm as beginner as can be and hadn't really thought much about that to be honest.
Method 1 - show/hide specific columns
So, the most direct way (and this will work if there are only a handful of things you want to show/hide), is just to create a boolean column for every thing you need to show/hide. So, if you had a phone number field, you could have a column called "show_phone_number", and when true it would show it.
Method 2 - show/hide whole sections
The next level that you might need is, rather than showing/hiding particular columns, have your show/hide boolean columns something like show_contact_info, show_photos, etc. for each logical section that a user would show or hide.
Then in your view, you'd have something like:
app/views/user/show.html.erb (or .haml or whatever you're using)
....
<% if #user.show_contact_info %>
<%= render :partial => "user_contact_info", :locals => {:user => #user} %>
<% end %>
app/views/partials/_user_contact_info.html.erb
<%=h user.email %><br />
<%=h user.phone_number %><br />
<%= user.blog_url %><br />
...
Method 3 - show/hide sections based on who is viewing it
Finally (and the code here is untested, but I think you'll get the idea) let's say your site has a social structure, and you want to show information to some people, but not to others. Basically you'll need the following in some form or another:
Section visibilities (who can view what sections)
Roles (friends, followers, public, private)
a few methods to make these relationships clear/easy to understand
So, in your User model you'd have something like:
class User < ActiveRecord::Base
has_many :friends, :through => "friendships" # or whatever construct you have
has_many :followers, :through => "followings" # or whatever construct you have
has_many :profile_visibilities
...
def is_friends_with(user)
friends.include?(user)
end
def is_a_follower_of(user)
user.followers.include?(self)
end
def can_see(visibilities)
visibilities.each do |v|
v.user == self || v.is_public || can_see_because_we_are_friends(v) || can_see_because_i_follow(v)
end
end
private:
def can_see_because_we_are_friends(visibility)
visibility.is_friend && is_friends_with(visibility.user)
end
def can_see_because_i_follow(visibility)
visibility.is_follower && is_follower_of(visibility.user)
end
end
Then a class called ProfileVisibilities:
class ProfileVisibilities < ActiveRecord::Base
belongs_to :user
...
def is_public
visibility == "public"
end
def is_friend
visibility == "friends"
end
def is_follower
visibility == "followers"
def is_private
!is_public && !is_friend && !is_follower
end
end
Then a table called profile_visibilities
id | user_id | profile_section | visibility
----------------------------------------------
1 | 1 | contact_info | public # <= visible to everyone
2 | 1 | personal_info | friends # <= visible only to "friends"
3 | 1 | blog_posts | friends # <= visible to "friends"
4 | 1 | blog_posts | followers # <= ...and followers
5 | 1 | photos | friends # <= visible only to "friends"
Then in your controller, something like:
app/controllers/users_controller.rb
...
def show
#user = User.find(params[:id])
#contact_info_visibilities = ProfileVisibilities.find(:all, :conditions = ['user_id = ? AND profile_section = "contact_info"', #user.id]
#photo_visibilities = ProfileVisibilities.find(:all, :conditions = ['user_id = ? AND profile_section = "photos"', #user.id]
# ... and more for each section visibility you need
end
...
And in your view:
app/views/user/show.html.erb
...
<% if current_user.can_see(#contact_info_visibilities) %>
<%= render :partial => "user_contact_info", :locals => {:user => #user}
<% end %>
<% if current_user.can_see(#photo_visibilities) %>
<%= render :partial => "user_photos", :locals => {:user => #user}
<% end %>
...
Related
At the moment I have a form in which the user can input price per person and/or duration and/or team_size. What I would like to accomplish is to retrieve all records from a table that match the user input and for this I set scope in the model:
scope :filter_by_team_size, -> (team_size) { where("team_size = ?", team_size) }
scope :filter_by_duration, -> (duration) { where("duration = ?", duration) }
scope :filter_by_price, -> (price) { where("price = ?", price) }
And then in the controller use that again to retrieve the records by doing so:
#experiences = policy_scope(Experience).order(team_size: :desc).geocoded.filter_by_team_size(params[:team_size]) if params[:team_size].present?
#experiences = policy_scope(Experience).order(duration: :desc).geocoded.filter_by_duration(params[:duration]) if params[:duration].present?
#experiences = policy_scope(Experience).order(price: :desc).geocoded.filter_by_price(params[:price]) if params[:price].present?
However, this only gives me only the records for which the first input value matches but ignores all other values. Additionally, when you are viewing the search results and use the filter again it should apply the filter only for the records it found already.
Any suggestion on how to solve this would be much appreciated!
One way to handle this is to use a virtual model that handles binding parameters to and from the form:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
end
You can then setup the form:
<%= form_with(model: (#search_query || SearchQuery.new), url: '/experiences', method: :get) %>
<div>
<%= f.label :team_size %>
<%= f.number_field :team_size %>
</div>
# ..
<% end %>
And then you can just bind the params to the model with ActionController::Parameters#permit just like you would with a normal ActiveRecord model:
class ExperiencesController
before_action :set_search_query, only: :index, if: ->{ params[:search_query].present? }
# ...
def index
#experiences = if #search_query
#search_query.build_scope(policy_scope(Experience))
else
policy_scope(Experience)
end.geocoded
end
private
def set_search_query
#search_query = SearchQuery.new(search_query_params)
end
def search_query_params
params.fetch(:search_query).permit(:team_size, :duration, :price)
end
end
This loopback will make the form stateful just like your normal CRUD forms. We have not actually implemented #build_scope yes so lets do so:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
def build_scope(base_scope)
compacted_attributes = attributes.reject { value.nil? || value.empty? }
compacted_attributes.each_with_object(base_scope) do |(k,v), base|
if base.respond_to? "filter_by_#{k}"
# lets you customize the logic with a scope
base.send("filter_by_#{k}", v) # the scope is responsible for ordering
else
# convention over configuration!
base.where(Hash[k,v]).order(Hash[k,:desc])
end
end
end
end
Since this uses convention over configuration you can get rid of those pointless scopes in your model.
In my Ruby on Rails application, I have a cinema system and am trying to return the screen a showing is in when a user searches for the showing.
To display the search drop down I am using this code in my _application.html.erb:
<%= render( :partial => '/screen_lookup', :locals => {:showings => #showings = Showing.all, :my_path => '/screens/display_screens_by_showing' })%>
Which renders the search from the _screen_lookup.html.erb:
<%= form_tag my_path, :method=>'post', :multipart => true do %>
<%= select_tag ('showings_id'),
options_from_collection_for_select(#showings, :id, :showing_times, 0 ),
:prompt => "Showings" %>
<%= submit_tag 'Search' %>
<% end %>
And uses the display_screens_by_showing in the screens_controller:
def display_screens_by_showing
#screens = Screen.showing_search(params[:showing_id])
if #screens.empty?
# assign a warning message to the flash hash to be displayed in
# the div "feedback-top"
flash.now[:alert] = "There are no films of that genre."
# return all products, in alphabetical order
#screens = Screen.all
end
render :action => "index"
end
And this searches using the method in the screen.rb model:
def self.showing_search(showing_id)
screen = Showing.where("id = ?", showing_id).screen_id
self.where("id = ?", screen)
end
Now, the problem I am having is that because a showing belongs_to a screen, and a screen has_many showings, I need to be able to search for the showing, and store that showing's screen_id in a variable to search for the screen that showing is in with, which I have tried doing in the model:
screen = Showing.where("id = ?", showing_id).screen_id
self.where("id = ?", screen)
But the error I am getting is:
NoMethodError in ScreensController#display_screens_by_showing
undefined method `screen_id' for #<ActiveRecord::Relation []>
These are the model relationships:
showing.rb:
class Showing < ActiveRecord::Base
belongs_to :screen
end
screen.rb:
class Screen < ActiveRecord::Base
has_many :showings
end
What code will get my search working?
The problem is that where doesn't return a record, it returns a relation that can be enumerated or chained, instead you want to use find or find_by to return a single record, which is kind equivalent to where + first
screen = Showing.find(showing_id).screen_id
which is sort of like doing
screen = Showing.where(id: showing_id).first.screen_id
If you want to pass a hash you can use find_by which will be like this
screen = Showing.find_by(id: showing_id).screen_id
PS:
I'm not sure what you're doing exactly, but i think those two lines can be merged into a single query ( not sure what it should be returning, but I'm assuming a screen )
def self.showing_search(showing_id)
Showing.find(showing_id).screen
end
I am trying to create a nested attribute form to create a model which is primarily an association "connector" between two other models. In my case, the models represent books, awards, and the "connector" model book_awards. When I am editing a book, I want to be able to quickly select which awards it has won.
I've been using
http://railscasts.com/episodes/196-nested-model-form-part-1
to help me get started, but I'm afraid I'm pretty much stuck.
Another SO question which seems similar is
accepts_nested_attributes_for with find_or_create? Unfortunately, it's also not quite what I'm doing and I haven't been able to adapt it.
My models look like this. Each model has additional attributes and validations etc, but I've removed them for clarity.
class Book < ActiveRecord::Base
has_many :book_awards
accepts_nested_attributes_for :book_awards, :allow_destroy => true
end
class Award < ActiveRecord::Base
has_many :book_awards
end
class BookAward < ActiveRecord::Base
belongs_to :book, :award
end
In my book controller methods for edit and new, and the failure cases for create and update I have a line #awards = Award.all.
In my view, I would like to see a list of all awards with check boxes next to them. When I submit, I would like to either update, create, or destroy a book_award model. If the check box is selected, I would like to update an existing model or create a new one if it doesn't exist. If the check box isn't selected, then I would like to destroy an existing model or do nothing if the award never existed. I have a partial for book_awards. I'm not sure if the check box selector should be in this partial or not.
I think my check box will be my hook to :_destroy but with its polarity reversed. I think something like this will basically do it:
= f.check_box :_destroy, {}, 0, 1
Currently, I have this in my partial but I'm not sure where it really belongs.
Next comes my view which currently doesn't work, but maybe it will help demonstrate what I'm trying to do. I loop through the awards and use a fields_for to set nested attributes for anything that already exists. It's horribly ugly, but I think it somewhat works. However, I don't really know how to get started with the else case.
= f.label :awards
- #awards.each do |a|
- if f.object.awards && f.object.awards.include?(a)
= f.fields_for :book_awards, f.object.book_award.select{|bas| bas.award == a } do |ba|
= render 'book_awards', :f => ba, :a => a
- else
= fields_for :book_awards do |ba|
= render 'book_awards', :f => ba, :a => a
I would prefer the awards to be listed in the same order each time (my #awards assignment in the controller will probably specify the order) as opposed to listing the existing awards first or last.
I hate to answer my own question, but I finally figured out something which works. The first thing I needed to do was to update the "new" case based on the crazy object which was included in the railscast. Next, I needed to manually set the :child_index. Finally, I needed to manually set the :_destroy check box appropriately.
.field
= f.label :awards
- #awards.each_with_index do |a,i|
- if exists = (f.object.awards && f.object.awards.include?(a))
- new_ba = f.object.book_awards.select{|s| s.award == a}
- else
- new_ba = f.object.class.reflect_on_association(:book_awards).klass.new
= f.fields_for :book_awards, new_ba, :child_index => i do |ba|
= render 'book_awards', :f => ba, :a => a, :existing => exists
My partial looks like this:
.field
= f.check_box :_destroy, {:checked => existing}, 0, 1
= f.label a.name
= f.hidden_field :award_id, :value => a.id
= f.label :year
= f.number_field :year
It's not horribly pretty, but it seems to do exactly what I wanted.
I have a Rails app that is using the simple_form gem. I have two models that are related, trades and stocks. In the form for trades, I want users to be able to enter their stock ticker symbol in a text field. Currently, I'm using the association function which renders a select box. The problem is that I want a text field instead since I have about a thousand stocks to choose from.
Is there a way I can do this (with or without Simple Form)?
the models:
class Trade < ActiveRecord::Base
belongs_to :stock
end
class Stock < ActiveRecord::Base
has_many :trades
end
the form on trades#new
<%= simple_form_for(#trade) do |f| %>
<%= f.association :stock %>
<% end %>
You should be able to just use this syntax:
<%= f.input :stock_id, :label => 'Enter your ticker:' %>
The problem here is that the user will not know what :stock_id is, as it's a reference to one of your many Stock objects.
So you probably want to implement a simple jquery autocomplete interface that returns a list of stocks like so:
[{:ticker => 'AAPL', :name => 'Apple Inc', :id => 1}, {:ticker => 'IBM', :name => 'International Business Machines', :id => 2}, etc ]
You can then display something like this as autocomplete results:
AAPL - Apple Inc
IBM - International Business Machines
and allow the user to select the one they are looking for. Behind the scenes you capture the :id and use that as your associated :stock_id.
You will need to add a stocks_controller action that takes a string and looks up Stocks based on a partial ticker and returns a max-number of stocks like 20.
def search
ticker_query = "%#{params[:ticker]}%"
stocks = Stock.where('ticker LIKE ?', ticker_query).limit(20)
render :json => stocks
end
This seems like it should be a common problem but I'm having trouble finding an answer. Basically I want to have a form with 10 or so checkboxes which I'm creating with check_box_tag. When the form is submitted I want to generate a query that return all records that match ANY of the checked selections. So, the number of checked selections will vary.
So, for example, if I have
class Book < ActiveRecord::Base
belongs_to :author
end
I want to generate something like
Book.where("author_id = ? or author_id = ?", params[authors[0]], params[authors[1]]) if there are two boxes checked, etc.
Thanks for any insight.
Will this work for you?
Book.where(author_id: [array_of_author_ids])
You need to collect author_ids from params first
I recently had to do something similar, this is how I achieved this. It's pretty clever (at least I think so. :))
I created a query model that serializes the query column (text field) in JSON. I use a form to get the query data from the user with selection fields.
class BookQuery < ActiveRecord::Base
has_many :books
# loop through each foreign key of the Book table and create a hash with empty selection
def self.empty_query
q = {}
Book.column_names.each do |column_name|
next unless column_name.ends_with?("_id")
q.merge column_name => []
end
end
end
I'm using Author as an example below:
<%= form_for #book_query do |f| %>
<% for author in Author.all %>
<%= check_box_tag "book_query[query][author_ids][]", author.id, false%>
<%= author.name %>
<% end %>
<%= f.submit "Save Query" %>
<% end %>
When this form is submitted you ended up with parameters like this:
When the form is submitted it generates this parameter:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"XXXXXXXXXXX", "book_query"=>{"query"=>{"author_ids"=>["2", "3"]}}, "commit"=>"Save Query"}
Now in the BookQuery controller's create action you can just do what create function always does:
def create
#book_query = BookQuery.build(params[:book_query])
if #book_query.save
flash[:success] = "Book query successfully saved."
redirect_to ...
else
flash[:error] = "Failed to save book query."
render :new
end
end
But by default rails serializes the data in hash type:
1.9.3p194 :015 > pp BookQuery.find(9).query
BookQuery Load (0.7ms) SELECT "book_queries".* FROM "book_queries" WHERE "book_queries"."id" = $1 LIMIT 1 [["id", 9]]
"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nauthor_ids:\n- '2'\n- '3'\n"
=> "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nauthor_ids:\n- '2'\n- '3'\n"
In BookQuery model, add the following:
serialize :query, JSON
But rail would change the IDs to string:
1.9.3p194 :018 > query = JSON.parse(BookQuery.find(10).query)
BookQuery Load (0.5ms) SELECT "book_queries".* FROM "book_queries" WHERE "book_queries"."id" = $1 LIMIT 1 [["id", 10]]
=> {"author_ids"=>["2", "3"]}
1.9.3p194 :019 > query["author_ids"]
=> ["2", "3"]
What I did then is override the attribute accessors in BookQuery model:
The below has to be done because the hash returns strings, not ids in integer.
def query=(query)
query.each_pair do |k, v|
if query[k].first.present?
query[k].map!(&:to_i)
else
query.except!(k)
end
end
write_attribute(:query, query)
end
# just want to avoid getting nil query's
def query
read_attribute(:query) || {}
end
To find book with this query, you can simply add this function to your Book model:
def self.find_by_book_query(book_query, options = {})
options[:conditions] = book_query.query
find(:all, options)
end
Now you get a customizable query string based on the model definition Book and everything works like the Rails way. :)