Hello I am new to rails and I'm creating an application to practice and learn. I have a issue which I don't know how to solve.
If I generate a model User and I want to include a section in the model where I name it investor_or_startup. When I create the form for a user to fill it out I want the user to only be able to fill in the blank with the word Investor or Startup. How would I go about this?
You could create a selector field in your form.
I would share some code but I think as a new rails developers, you should get used to reading and understanding the docs, as that is invaluable.
http://guides.rubyonrails.org/form_helpers.html#making-select-boxes-with-ease
Add a validation in your model for that specific attribute, if it's called investor_or_startup, then using validates specify this attribute, use the format option, and optionally you can add a message for the user, like:
class User < ApplicationRecord
validates :investor_or_startup,
format: {
with: /Investor|Startup/,
message: 'Two valid options are Investor or Startup'
}
end
The with option inside format accepts a regular expression, in this case to check for the specific word Investor or (|) Startup, it's case sensitive, so lowercase letters would make an invalid input. If you want to change to case insenstive then consider adding i at the end of your expression, like /investor|startup/i.
The message options accepts an string which you can use to display a message for the user.
You can keep reading about this here.
The most common way of doing thin in rails would be to add a role attribute to your User model. In this case, role replaces start_up_or_investor. You would start by using a migration to add a field called role of type :integer to your users table.
Next you would add the following line to your User model:
enum role: [:investor, :start_up]
You can read more about enum here.
Finally, you can use a select_tag in a Rails form helper to select one of the enum options. There are many ways to use a select_tag with an enum. A quick internet search will reveal many blogs and SO posts on the subject.
Bottom line: I recommend using an enum for this sort of thing.
So you have a User model and that user could possibly be an investor or a startup. To start off you could add some kind of "type" attribute that will query whether the user is a startup or an investor. User.type #=> "startup" or User.type #=> "investor". From there you could set up your form_for user and use a select field that would allow the user to select their value for the type attribute <%= f.select(:type, ...) %>
Like Sam suggested, I encourage you to study the rails guides,(particularly form helpers) to help you understand how a form works.
Adding roles to uses is a common programming problem. There are two decent ways to solve this (and many bad ways).
1. Use an ActiveRecord::Enum
class User < ApplicationRecord
enum role: [:investor, :start_up]
end
An enum is a integer column that maps to specific values. In this case investor = 0, start_up = 1.
To create an input based on this you can use the enum mapping:
<%= f.select(:role, User.roles.keys.map {|role| [role.titleize,role]}) %>
While simple the major limitation is that a user can only ever have one role.
2. Setup a many-to-many association
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
scope :with_role ->{|*names| joins(:roles).where(name: names) }
def has_role?(name)
roles.where(name: name).exist?
end
end
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
class Role < ApplicationRecord
has_many :user_roles
has_many :users, through: :user_roles
end
This lets you add and remove roles from a user seperately and often is a better fit for real world use cases. The Rolify gem is an excellent example of this pattern.
To allow a user to select roles you can use a checkbox:
<%= f.collection_check_boxes(:role_ids, Role.all, :id, :name) %>
Related
So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly
I have a Review model. My users should be able to write reviews. The view for the Review#New should be a form with textfields that the admin creates beforehand.
In other words, my admin-user should be able to create multiple instances of a Review model that has different fields, perhaps even of different input types (string, integer, etc.). That way, when a regular user logs in, they see the different form fields that were specified for data collection by the admin user.
Naturally all of that should be stored in the DB for retrieval within the context it was stored (aka for that specific model).
What's the best way to approach this in Rails?
Think of it like a survey form, and a survey form builder.
It would be good if I could do this with Simple-Form, but that's not a requirement.
Edit 1
Here is an example of the type of fields that they should be able to add to a review:
In my experience a good portion of database design is helped by simply finding the right name for things. In your case I think you are on the right track with thinking about surveys or quizzes.
Check out the survey gem for ideas. In it the base model is Surveys. Surveys have many Questions. Questions have many Options. Surveys also have many Attempts which are answered surveys. Attempts then have many Answers.
So the corollary for you could be to have Reviews/Evaluations (created by admins) which might have many Criteria/Inquiries (possibly of different types, but we'll get to that in a minute). Then your users would create Responses/Assessments which would belong to a specific Review/Evaluation and have many Answers/Responses.
For different question types (Short Answer, Likert Scale Rating, 1-10, Tag List, etc) you could use polymorphism on the criteria/inquiries.
Hopefully some of these names I've used will help you. Feel free to use a thesaurus for more inspiration.
EDIT Re:Polymorphism
Disclaimer: polymorphism might be overkill depending on your application.
Sure, I'll expand some. Not exactly. Take a look at the rails guide on polymorphism if you haven't already. I think what you would want is
class Criterion < ActiveRecord::Base
belongs_to :askable, polymorphic: true
end
Then then I would make a model for each question/criterion type. For example:
class ShortAnswer < ActiveRecord::Base
has_many :criteria, as: :askable
end
class Likert < ActiveRecord::Base
has_many :criteria, as: :askable
end
Side note: If rails does not properly pluralize criterion to criteria you may need to add the following to your config/initializers/inflections.rb file
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'criterion', 'criteria'
end
Scratch solution.
From my experience the easiest solution is to use hstore, json or jsonb type of fields.
This solution play good with Postgresql database.
To achieve this approach you need to add field to your Review model.
Migrations:
# Reviews
def change
add_column :reviews, :structure, :json
end
# Answers
def change
add_column :answers, :values, :hstore
end
Then you can define model ReviewStructure plain ruby class, here you can use Virtus gem to serialize it easely:
class ReviewStructure
include Virtus.model
attribute :fields, Array[Field]
class Field
include Virtus.model
attribute :name
attribute :type
end
end
Then define in Review the serialization for structure field:
class Review < ActiveRecord::Base
...
serialize :structure, ReviewStructure
end
Then you can access structure fields of review with review.structure.fields.
In view you can use a simple form
<% simple_form_for #answer do |f| %>
<% #review.structure.fields.each do |field| %>
<% f.input "values[#{field.name}]", as: field.type %>
<% end %>
<% end %>
To access answer results just use:
answer.values.each do |field_name, value|
...
end
Note:
As for admin for it's better to handle creation of review structure on client side(using js), and post pure JSON structure via API.
With such approach you will have ability to create quizzes with different types of field.
Note:
Please keep in mind that current implementation connect one review to one answer, assuming that the answer model contains all the values of user response.
I'm trying to extend a model in rails.
Model User uses table users in database with field :username, :password.
class User < ActiveRecord::Base
end
Model SuperUser uses table super_users in database with fields :user_id, :name:
class SuperUser < ActiveRecord::Base
belongs_to :user
end
I would like the SuperUser to be an extension of User so as to be able to do this :
SuperUser.create(:name => "foo", :username => "bar", :password => "foobar")
or when I fetch data to get something like this
> s = SuperUser.find 1
> s.username
> "bar"
Has anyone any idea how I can do this?
You need to consider two possible patterns of extending your models. One of them, Single Table Inheritance is built into Rails and is well documented. The other pattern, the Multi Table Inheritance, which is surprisingly less welcomed in Rails. You need to implement it yourself or use less popular gem for it.
One of the reasons why, I think, MTI is not well supported in Rails is related to performance. For example, even simple User.find 1 should be a lookup in two tables.
What you ask in your question (having two tables) is MTI. But why not STI in this simple case? Are User and SuperUser as deviated from each other that you worry about saving space and using two separate tables for them?
For me it looks like going with a single users table, and adding type column to it.
add_column :users, :type, :string
Now:
class User < ActiveRecord::Base
end
class SuperUser < User
end
and it just works.
You'd want to use either Single Table Inheritance (STI) or Multiple Table Inheritance (MTI). This is not as complicated as it may seem at first. You can read about it here
I have two types of users (regular user, super user). What is the proper way to extend one base user class with additional tables?
I was thinking something like this but I am not sure am I going to right direction:
class User < ActiveRecord::Base
end
class SuperUser < User
end
class RegularUser < User
end
Is this the proper way to do it in Rails? Thanks :)
It is 100% correct approach, however you need to remember, that all your models will be stored in one table in database. This approach is called STI (Single table inheritance) and requires only one additional field type in you model.
If you want to have different types of users I would go with user roles versus different user tables etc.
A very good gem for that is CanCan and the documentation is excellent:
https://github.com/ryanb/cancan/wiki/Role-Based-Authorization
You will also have nice helpers as .can? or .cannot? and more.
Yes, and you should also use single table inheritance. What this means is you should add a column called 'type' to your user model. Rails recognizes the column 'type' and treats it special. Essentially, all entries in your type model will reference another model. In that model, you can define rules for each type. It would also be a good idea to validate your user model so that only the two types you want can be entered. This should work:
class User < ActiveRecord::Base
validates :type, :inclusion => {:in => ['SuperUser', 'RegularUser']}
end
I have a very simple model
class Lifestyle < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :profiles
end
that has a has_and_belongs_to_many relationship with Profile
class Profile < ActiveRecord::Base
attr_accessible ...
belongs_to :occupation
has_and_belongs_to_many :lifestyles
accepts_nested_attributes_for :lifestyles
end
I want to use ActiveAdmin to edit the Profile object, but also assign Lifestyles to a profile. It should be similar to dealing with belongs_to :occupation, as this is sorted out automatically by ActiveAdmin to a dropbox with the options pre-filled with available occupations.
I've tried to use the has_many form builder method, but that only got me to show a form to type in the name of the Lifestyle and on submission, it returned an error.
f.object.lifestyles.build
f.has_many :lifestyles do |l|
l.input :name
end
Error I get:
Can't mass-assign protected attributes: lifestyles_attributes
The perfect way for me would be to build several checkboxes, one for each Lifestyle in the DB. Selected means that the lifestyle is connected to the profile, and unselected means to delete the relation.
I'm having great doubts that this is possible using ActiveAdmin and without having to create very complex logic to deal with this. I would really appreciate it if you'd give your opinion and advise me if I should go this way or approach it differently.
After some research, I am ready to answer my own question.
First, I have to say thanks to #Lichtamberg for suggesting the fix. However, that only complicates things (also regarding security, though not an issue in this case), and doesn't help me reach my ideal solution.
Digging more, I found out that this is a very common scenario in Rails, and it's actually explained in Ryan Bates' screencast no #17.
Therefore, in Rails, if you have a has_and_belongs_to_many (short form HABTM) association, you can easily set the ids of the other associated object through this method:
profile.lifestyle_ids = [1,2]
And this obviously works for forms if you've set the attr_accessible for lifestyle_ids:
class Profile < ActiveRecord::Base
attr_accessible :lifestyle_ids
end
In ActiveAdmin, because it uses Formtastic, you can use this method to output the correct fields (in this case checkboxes):
f.input :lifestyles, as: :check_boxes, collection: Lifestyle.all
Also, I have simplified my form view so it's now merely this:
form do |f|
f.inputs # Include the default inputs
f.inputs "Lifestlyes" do # Make a panel that holds inputs for lifestyles
f.input :lifestyles, as: :check_boxes, collection: Lifestyle.all # Use formtastic to output my collection of checkboxes
end
f.actions # Include the default actions
end
Ok, now this rendered perfectly in the view, but if I try and submit my changes, it gives me this database error:
PG::Error: ERROR: null value in column "created_at" violates not-null constraint
: INSERT INTO "lifestyles_profiles" ("profile_id", "lifestyle_id") VALUES (2, 1) RETURNING "id"
I found out that this is due to the fact that Rails 3.2 doesn't automatically update the timestamps for a HABTM association table (because they are extra attributes, and Rails only handles the _id attributes.
There are 2 solutions to fix this:
Either convert the association into a hm:t (has_many, :through =>)
Or remove the timestamps from the table
I'm going to go for 2) because I will never need the timestamps or any extra attributes.
I hope this helps other people having the same problems.
Edit: #cdesrosiers was closest to the solution but I already wrote this answer before I read his. Anyway, this is great nevertheless. I'm learning a lot.
Active Admin creates a thin DSL (Domain-Specific Language) over formtastic, so it's best to look at the formastic doc when you need form customization. There, you'll find that you might be able to use f.input :lifestyles, :as => :check_boxes to modify a has_and_belongs_to_many relationship.
I say "might" because I haven't tried this helper myself for your particular case, but these things have a tendency to just work automagically, so try it out.
Also, you probably won't need accepts_nested_attributes_for :lifestyles unless you actually want to modify the attributes of lifestyles from profiles, which I don't think is particularly useful when using active admin (just modify lifestyles directly).
Add
attr_accessible :lifestyles_attributes
f.e.:
class AccountsController < ApplicationController
attr_accessible :first_name, :last_name
end