Rails - Polymorphic Association to Create Different Profiles - ruby-on-rails

Attempting to make it so that when a user is created, based on whether they select to be a student or a corporate, rails will create that user either a student profile or a corporate profile.
Ive tried to set it up using Polymorphic associations however cant figure out how to generate the profile at the model layer based on what is selected in the view.
Models
class User < ActiveRecord::Base
has_secure_password
has_one :student_profile, dependent: :destroy
has_one :corporate_profile, dependent: :destroy
has_many :searches, dependent: :destroy
#attr_accessor :profile_type - removed due to Rails 4, pushed strong params in controller
before_create :create_profile
def create_profile
if profile_type == 1
build_student_profile
else
build_corporate_profile
end
end
end
Student and Corporate Profiles
class CorporateProfile < ActiveRecord::Base # or possibly inherit from ActiveRecord::Base if not using inheritance
belongs_to :user
end
class StudentProfile < ActiveRecord::Base # or possibly inherit from ActiveRecord::Base if not using inheritance
belongs_to :user
end
View
Here i have two radio buttons to decide which user type on the sign up form
<%= bootstrap_form_for(#user) do |f| %>
<div class="field">
<%= f.form_group :gender, label: { text: "Gender" }, help: "Are you a corporate or a student?" do %>
<p></p>
<%= f.radio_button :profileable, 1, label: "Student", inline: true %>
<%= f.radio_button :profileable, 2, label: "Corporate", inline: true %>
<% end %>
</div>
Users Controller
class UsersController < ApplicationController
def index
#users = User.paginate(page: params[:page], :per_page => 5).includes(:profile)
end
def show
if params[:id]
#user = User.find(params[:id])
# .includes(:profile)
else
#user = current_user
end
#searches = Search.where(user_id: #user).includes(:state, city: [:profile])
end
def new
#user = User.new
##corporateprofile = Corporateprofile.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to widgets_index_path
else
redirect to '/signup'
end
end
private
def user_params
params.require(:user).permit(:firstname, :lastname, :email, :password, :profile_type)
end
end
And there is no passing code on the controller (as im stuck on that). Any better suggestion or ways to fix this would be much appreciated!
Cheers

First of all, you want to rename your profile classes to StudentProfile and CorporateProfile. This will necessitate running migrations to change your table names too.
The answer to this question depends on how different you want StudentProfile and CorporateProfile to be. If they are completely different or even mostly different, make them separate classes. If they are mostly the same (in other words, they share many of the same methods) you should create a Profile (or UserProfile) model and have StudentProfile and CorporateProfile inherit from this model.
As for implementation, it should look something like this:
# user.rb
class User < ActiveRecord::Base
has_one :student_profile
has_one :corporate_profile
attr_accessor :profileable #probably change this to profile_type. Using attr_accessible because we want to use it during creation, but no need to save it on the user model, although it may not be a bad idea to create a column for user model and save this value.
before_create :create_profile
def create_profile
if profileable == 1
build_student_profile
else
build_corporate_profile
end
end
end
# student_profile.rb
class StudentProfile < UserProfile # or possibly inherit from ActiveRecord::Base if not using inheritance
belongs_to :user
# other student profile stuff here
end
And corporate profile model would look the same as student profile.
Also, you should be using Rails 4 at this point, especially if you're learning and don't understand controllers and params, as this is pretty different between rails 3 and 4. No use in learning something that's outdated, right?
Edit: I should mention, I don't thing you're understanding rails polymorphism. A model should be polymorphic when it will belong to multiple models, not when it will have different subclasses.
For example, if your app has a Like model and something else like a Post model, and a user can like other users' profiles or posts, that might be a good candidate for polymorphism, because Like may belong to StudentProfiles or CorporateProfiles or Posts.

Related

Rails - How to create multiple "matches" in one action between a user and opportunities?

I'm looking for the easiest and the most clever way to create interest_id(match) in one-click.
Here is my MVC :
user.rb
class User < ApplicationRecord
has_many :interests, through: :opportunities
end
interest.rb
class Interest < ApplicationRecord
belongs_to :opportunity
belongs_to :user
end
opportunity.rb
class Opportunity < ApplicationRecord
has_many :interests
end
InterestsController.rb
def create
#user = current_user
#opportunities = Opportunity.all
#interest = Interest.new(interest_params)
if #interest.save!
redirect_to user_interests_path, notice: 'it works'
else
render :new, notice:"it doesn't work"
end
end
def interest_params
params.permit(
:user_id,
:opportunity_id)
end
user/show
<%= link_to "Match", user_interests_path(#user), class:"btn btn-primary", :method => :post %>
For now, I can't pass opportunities (nil). Could you please advise me about the easiest way to create interests? (New on RoR for 6 months).
Many thanks for your help.
If I understand correctly your relation schema, the Interest is the join record associating a User to (eventually) many Opportunity, and vice-versa (many-to-many relationship).
With that being said (and please correct me if I am wrong), you can do the following to achieve what you want:
# in user/show
<% #opportunities.each do |opportunity| %>
<%=
link_to "Match opportunity #{opportunity.id}",
user_interests_path(#user, opportunity_id: opportunity.id),
class: "btn btn-primary",
method: :post
%>
<% end %>
# in interests_controller
def create
if current_user.interests.create(opportunity_id: opportunity_id_param)
redirect_to user_interests_path, notice: 'it works'
else
render :new, notice: "it doesn't work"
end
end
private
def opportunity_id_param
params.require(:opportunity_id)
end
This suggested code:
requires the opportunity_id param for the interests#create action
use current_user to automatically set the user_id on the Interest model, so the end-users can't send a user_id that are not theirs (if they could, then each user could create interest for other users without their agreement... security flaw)
On a side note, I strongly advise you to not select all existing Opportunity record and display it on your page: it does not scale. Someday, you will end up with hundreds of Opportunity records, making this list too big from a User Experience perspective.
I suggest a smarter approach, for example some kind of ordering + limit: max of 10 records ordered by "most interest", which can be accomplished by the following:
# in controller
#popular_opportunities = Opportunity
.joins('LEFT JOINS interests ON interests.opportunity_id = opportunities.id')
.order('count(interests.*) DESC, opportunities.id')
.limit(10)
And then in the view, simply use #populator_opportunities instead of #opportunities.
Other options, like pagination, are also efficient in this case but IMO relevant ordering is the minimum.
First, you need to pass the ids of the opportunities you want to create interest some way, the best is a form, with checkboxes like MrShemek said, or a multi select dropdown.
I think you probably made some mistakes in User and Opportunity with the has_many and belong_to part:
class User < ApplicationRecord
has_many :interests
has_many :opportunities, through: :interests
# interest is the one that links user and opportunity, it has the references for both user and opportunities
end
class Opportunity < ApplicationRecord
has_many :interests
has_many :users, through: :interests
end
then in controller you could do
def create
#user = current_user
#opportunities = Opportunity.all
#user.opportunity_ids = interest_params[:opportunity_ids] # it will create the interrests automatically for the given ids (because the relations of has_many through)
if #user.save!
redirect_to user_interests_path, notice: 'it works'
else
render :new, notice:"it doesn't work"
end
end

belongs_to with accepts_the_nested_attributes for is not saving foreign key in object

I have a model named Profile which has belongs_to relation with Address
class Profile < ActiveRecord::Base
belongs_to :address, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
here is the code in controller
def create
#profile = Profile.new(profile_signup_params)
#profile.save
respond_to
..... etc.....
end
for params
def profile_signup_params
params.require(:profile).permit( { address_attributes: [:country]
end
but #profile.save
i get this object
#<MemberProfile:0x0000000af135b0
id: 28,
address_id: nil,
birth_date: nil,
country_code: nil,
phone: nil,
stripe_customer_id: "123",
created_at: some time,
updated_at: some time>
as you cane see this address_id is nil
Profile is created
Address is created
but Address is not assigned to Profile
Please help me, what thing i am doing wrong
I think You made a Wrong association between Address and Profile
Profile which has belongs_to relation with Address instead It should be Profile has has_one association with respect to Address.
As mention in official Documentation Active Record Nested Attributes
class Profile < ActiveRecord::Base
has_one :address, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
...
end
class Address < ActiveRecord::Base
belongs_to :profile
end
Rest Controller and model code would be same in your case except you need to a primary-foreign key relation between Address and Profile; need to create profile_id column in address table.
Note: Make sure if there is a uniqueness you need to follow Uniqueness Gotcha!!! in ActiveRecord NestedAttributes.
Original Blog Uniqueness Gotcha!!! Problem and Solution
Hope this This Help you!!!
To add to Vinay's answer (which is correct IMO), you'd want to make sure you're passing the right data through your controller.
Whilst the belongs_to association should allow you to set the nested parameters you require, it would be prudent to mention what Vinay said -- if you're creating an Address for a Profile, surely it would be the address that would belong to the Profile?
You can see about the has_one association here:
You'd handle it in a very similar way:
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
def new
#profile = Profile.new
#profile.build_address
end
def create
#profile = Profile.new profile_params
#profile.save
end
private
def profile_params
params.require(:profile).permit(address_attributes: [:country])
end
end
#app/views/profiles/new.html.erb
<%= form_for #profile do |f| %>
<%= f.fields_for :address do |a| %>
<%= a.text_field :country %>
<% end %>
<% end %>
This, with the model code from Vinay should get it working properly.

Rails - how to show attribute of an associated model

I am trying to make an app in Rails 4.
I just asked this related question and got a clear answer. It seems I can't understand how to take that logic and apply it elsewhere.
Rails How to show attributes from a parent object
I have a user model, profile model a projects model and a universities model.
Associations are:
Profile belongs to university
Profile belongs to user
University has many profiles
University has many projects
Projects HABTM user
Projects belong to universities
In my projects controller, I define #creator as follows:
def create
logger.debug "xxx create project"
#authorise #project
#project = Project.new(project_params)
#project.creator_id = current_user.id
#project.users << current_user
respond_to do |format|
if #project.save
format.html { redirect_to #project }
format.json { render action: 'show', status: :created, location: #project }
else
format.html { render action: 'new' }
format.json { render json: #project.errors, status: :unprocessable_entity }
end
end
end
I try to define creator_profile like this:
def show
#authorise #project
#project = Project.find(params[:id])
#creator = User.find(#project.creator_id)
#creator_profile = #creator.profile
end
In my uni table, I have attributes called logo and name. I use avatar uploader in which i have logo defined (that's why I have two .logo below).
In my projects, show, I want to display the university that the project creator belongs to.
I've tried this:
<%= image_tag(#creator_profile.university.logo.logo) %>
<div class="generaltext"><%= #creator_profile.university.name %> </div>
I get this result: undefined method `logo' for nil:NilClass
Based on the link to my problem above
<%= image_tag(creator_profile.university.logo.logo) %>
<div class="generaltext"><%= creator_profile.university.name %> </div>
I get this result:
undefined local variable or method `creator_profile' for #<#<Class:0x007f998f17ad88>:0x007f998d1ce318>
I'm not sure I understood the very detailed explanations given in the answer to my previous question. If the first version is right, then I don't understand the explanation at all. If the second version is right, then why does this error message come up?
Im wondering if the problem arises out of there not being an association between university and user? I was hoping, based on the user who created the project, to find the uni that the creator belongs to.
That's why i tried:
<%= image_tag(creator_profile.project.university.logo.logo) %>
<div class="generaltext"><%= creator_profile.project.university.name %> </div>
I get this error:
undefined method `project' for #<Profile:0x007f998ada41b8>
I think that you need to understand some basic concepts of Ruby and Ruby and Rails to solve this question yourself.
In ruby, vars with # are instance variables and are available all over the class. That means that they will be available in your view if you declare them in your controller.
EG #creator_profile = #profile.user
On the other hand, vars without # are only available inside the same block.
An example:
#controller
#users = User.all ##users, instance variable
#view
<% #users.each do |user| %>
<h3><%= user.name %></h3> #user, local variable. This will work
<% end %>
<h3><%= user.name %></h3> #this won't work because it is outside the block
Google about ruby vars and scopes.
Also, I think that you are relying too much on 'rails magic' (or you are skipping some code lines), if you don't declare an instance var, it won't exist. Naming conventions don't work that way.
At last but not at least, having a look at your relations, I think that they need some refactor. Also the use of singular and plural is not correct. I know that it's not real code but it denotes that they don't reflect real relationships between entities.
Don't try to make 'octopus' models, where everybody belongs to everybody, and think about the relationships itself, not only trying to associate models. EG:
Profile
belongs_to :creator, class_name: 'User'
This way you can write:
#controller
#profile_creator = Profile.find(params[:id]).creator
#view
#profile_creator.university
You will understand better what you are doing.
Hope it helps.
It seems I can't understand how to take that logic and apply it elsewhere.
I don't think you appreciate how ActiveRecord associations work in Rails. I'll explain further down the page.
Your associations will be the likely cause of the problem.
Setting up complicated associations is always tricky - it's best to keep the data as separate as possible.
Here's how I'd construct the models / associations:
#app/models/university_student.rb
class UniversityStudent < ActiveRecord::Base
belongs_to :university
belongs_to :student, class_name: "User" #-> student_id
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :placements, class_name: "UniversityStudent", foreign_key: :student_id #-> user.placements
has_many :universities, through: :placements #-> user.universities
has_and_belongs_to_many :projects #-> user.projects
has_one :profile #-> user.profile (avatar etc)
has_many :created_projects, class_name: "Project", foreign_key: :creator_id
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user #-> store avatar here. This can be used across entire app
end
#app/models/university.rb
class University < ActiveRecord::Base
has_many :projects
has_many :students, class_name: "UniversityStudent" #-> university.students
end
#app/models/project.rb
class Project < ActiveRecord::Base
belongs_to :university
belongs_to :creator, class_name: "User" #-> creator_id
has_and_belongs_to_many :users
delegate :profile, to: :creator, prefix: true #-> #project.creator_profile
end
This allows you to do the following:
def create
#project = curent_user.created_projects.new project_params
#project.users << current_user
Because the associations actually associate your data, you'll be able to do the following:
def show
#project = Project.find params[:id]
##creator_profile = #project.creator.profile
#creator_profile = #project.creator_profile #-> if you use the delegate method outlined in the models
end
--
In my projects, show, I want to display the university that the project creator belongs to.
#app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def show
##project = Project.find params[:id]
#project = current_user.created_projects.find params[:id]
end
end
#app/views/projects/show.html.erb
<%= #project.creator.universities.first %>
My code above allows for multiple universities. Thinking about it, it should be limited to one, but I'll leave it as is for now, maybe change it later.
In my uni table, I have attributes called logo and name. I use avatar uploader in which i have logo defined (that's why I have two .logo below).
Don't use two logo method, it's an antipattern (explained below)
The fix for this is two-fold:
Firstly, make sure you're calling #creator_profile.university with the following:
<%= #creator_profile.university %>
If this works, it means you have a problem with .logo.logo (detailed below), if it doesn't, it means you've not defined #creator_profile or the university association correctly.
Secondly, you need to ensure you have the correct controller/view setup.
The problem for many people - especially beginners - is they simply don't understand the way Rails works with controllers & views. You need to appreciate that each time you render a view, the only data it has access to is that which you define in the corresponding controller action...
#app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def show
#project = Project.find params[:id]
#creator_profile = #project.creator_profile
end
end
#app/views/projects/show.html.erb
<%= content_tag :div, #creator_profile.universities.first.name, class: "generaltext" %>
Trivia
#project.creator_id = current_user.id
This should not have to be defined.
You should be able to change the foreign_key in the association, so that Rails will automagically define the creator_id for you:
#app/models/project.rb
class Project < ActiveRecord::Base
belongs_to :creator, class: "User" #-> foreign_key should be :creator_id
end
#app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def create
#project = current_user.created_projects.new project_params #-> populates foreign key automatically.
--
.logo.logo
This is an antipattern.
Calling the same method twice is simply bad practice - why are you doing it?
You either want to delegate any recursive data you're trying to access (such as the example with .creator_profile above), or you'll want to restructure that functionality.
You want the following:
If you have to delegate to an assets model, you could get away with the following:
<%= #creator_profile.university.images.logo %>

Authlogic with nested attributes and polymorphic associations

I'm having trouble with the following code:
User < AR
acts_as_authentic
belongs_to :owner, :polymorphic => true
end
Worker < AR
has_one :user, :as => :owner
accepts_nested_attributes_for :user
end
Employer < AR
has_one :user, :as => :owner
accepts_nested_attributes_for :user
end
I'd like to create registration forms based on user types, and to include authentication fields such as username and password. I currently do this:
UserRegistrationController < AC
#i.e. a new Employer
def new
#employer = Employer.new
#employer.build_user
end
...
end
I then include User fields with fields_for. All views render fine, but here's the catch: I cannot build a User, it tells me :password is a wrong method, so I guess the authentication logic has been bypassed.
What should I do? Am I doing it wrong altogether? Should I drop polymorphic associations in favor of Single Table Inheritance? Whatever I do, I have to make sure it plays nicely with Authlogic.
I'd approach the building of new users of either type in the opposite direction. ie:
#controller
#employer = Employer.new
#user = #employer.build_user
#view
form_for #user |f|
f.text_field :login
f.password_field :password
fields_for :owner, #employer |f_e|
f_e.some_field :some_value
#controller
def create
#owner = params[:owner][:some_employer_field_or_virtual_attribute] ? Employer.new params[:owner] : Worker.new params[:owner]
#owner.save
#user = User.new(params[:user].merge!(:owner => #owner)
if #user.save
...
re. mentioned virtual attribute - if there's no field in the model, and thus in the form, which distinguishes user type as employer or worker then set an virtual attribute within each which you can put as a hidden boolean field in the form

Using accepts_nested_attributes_for + mass assignment protection in Rails

Say you have this structure:
class House < ActiveRecord::Base
has_many :rooms
accepts_nested_attributes_for :rooms
attr_accessible :rooms_attributes
end
class Room < ActiveRecord::Base
has_one :tv
accepts_nested_attributes_for :tv
attr_accessible :tv_attributes
end
class Tv
belongs_to :user
attr_accessible :manufacturer
validates_presence_of :user
end
Notice that Tv's user is not accessible on purpose. So you have a tripple-nested form that allows you to enter house, rooms, and tvs on one page.
Here's the controller's create method:
def create
#house = House.new(params[:house])
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
Question: How in the world would you populate user_id for each tv (it should come from current_user.id)? What's the good practice?
Here's the catch22 I see in this.
Populate user_ids directly into params hash (they're pretty deeply nested)
Save will fail because user_ids are not mass-assignable
Populate user for every tv after #save is finished
Save will fail because user_id must be present
Even if we bypass the above, tvs will be without ids for a moment of time - sucks
Any decent way to do this?
Anything wrong with this?
def create
#house = House.new(params[:house])
#house.rooms.map {|room| room.tv }.each {|tv| tv.user = current_user }
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
I haven't tried this out, but it seems like the objects should be built and accessible at this point, even if not saved.

Resources