I am trying to use form.collection_select in an order form with data from a second model named products. I would like to use the field "product_reference" from the products table as selector in my HTML page.
When in the order form, I want to present all product_references to the user in a selctor, let the user make his choice, take this product_reference, and store the corresponding product_id in the order form (and database table field).
I have two tables, connected by id as in this schema:
create_table "orders", force: :cascade do |t|
t.datetime "order_date"
t.bigint "product_id", null: false
...
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_id"], name: "index_orders_on_product_id"
end
create_table "products", force: :cascade do |t|
t.string "brand"
t.string "product_reference"
t.string "description"
...
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_reference"], name: "index_products_on_product_reference", unique: true
end
with the relationships described this way:
app/models/order.rb
class Order < ApplicationRecord
belongs_to :product
end
app/models/ product.rb
class Product < ApplicationRecord
has_many :orders
end
I used the rails console, and got exactly what I want this way:
Product.all.map(&:product_reference)
Product Load (0.8ms) SELECT "products".* FROM "products"
=>
["C-Beau33",
"C-Beau50",
...
"B-Volk33",
"B-Volk66"]
In my controller I have tried to assign #prodrefs with this dezired result, which I could then use in the collection_select:
Order Controller:
# GET /orders/new
def new
#order = Order.new
#prodrefs = Product.all.map(&:product_reference)
end
I used this in my html view:
app/views/orders/_form.html.erb
<%= form.collection_select :order_id, Product.all, :id, #prodrefs, :prompt => 'Select Ref' %>
and step on this error message:
["C-Beau33", "C-Beau50", ... "B-Volk33", "B-Volk66"] is not a symbol nor a string
I think this error msg is not precisely linked to my problem, when trying just to add ".to_s" I only get another error on
undefined method `#<Product::ActiveRecord_Relation:0x00007fa1087ddda8>' for #<Product id: 1, brand: "Beaufort", product_reference: "C-Beau33",...>
What has been done so far:
in an older approach with a foreign key directly on this "product_reference", I was able to use this code in my view, worked like a charm, user could select the product_reference from the pull down menu in his browser:
<%= form.collection_select :fk_prodref, Product.order(:product_reference), :product_reference, :product_reference, :prompt => 'Select Ref' %>
This fk_approach has now been rejected, cause some extensions were necessary, with new model associations.
Looking at the RubyOnRails Guide, I can find the example on cities, and the explanation in ActionView::Helpers::FormBuilder page, I see the description as:
collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
So I believe my text_method is the problem here. When I translate from the guide, I come to this logic:
collection_slct html in city example
method -> <option value=>
collection -> Berlin
value_method -> <select ... id="city_id">
text_method -> <select name="city_id">
which let's me believe, that I want to have the 2nd row (collection) as all my product references. But I ammissing the point on the text_method then, cause the error message directs me there. Any help highly appreciated
You're close. Kind of a mish-mash of several ways that can work, but you're close.
You can do this in a controller if you choose, though it would just be
#products = Product.all.order(:id). # or whatever order you would like
Then in your form
<%= form.collection_select( :product_id, #products, :id, :product_reference, prompt: 'Select Ref') %>
The first argument is the attribute you are saving, in your case,
where you have order_id above, it's really product_id.
The section argument is the list of objects for the collection
The third argument is the attribute from the collection you need to save to the product_id attribute. In this case, it is id
The fourth argument is the attribute name you would like displayed in the dropdown, :product_reference
Lastly the fifth option is for the prompt
Related
I have roles for users implemented with an enum in the user model:
enum role: [:staff, :clinician]
I have a University model with the User belongs_to :university and a University model with has_many :users.
The way that my app will work is that "staff" will belong to a university, but "clinicians" are private practice and therefore do not need to belong to a university and need not select one during signup.
I have my signup form set up to hide the university field if the user selects Clinician, but I want to make sure that my validations are set up to require that any user who selects staff on signup must also select a university and that any user who selects clinician on signup fails validation if they select a university.
Here's the role section of the user signup form:
<%= f.label :role %>
<%= f.select(:role, User.roles.keys.map {|role| [role.titleize,role]}, :include_blank => "Please Select", id: 'user_role') %>
<%= content_tag :div, class: 'js-dependent-fields', data: { 'select-id': 'user_role', 'option-value': 'staff'} do %>
<%= f.label :university_id%>
<%= collection_select( :user, :university_id, University.all, :id, :name, prompt: true) %>
It requires a bit more extra setup but I think pays off in flexibility over time:
Try Single Table Inheritance combined with your enum roles. You'll be able to more easily define separate callbacks, validations, scopes, and associations for your different roles, while inheriting the ones you want them to share in common. For example, you could just make it so only Staff belongs_to :university, and Clinician does not.
# Stripped down schema
create_table "universities", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "university_id"
t.integer "role"
t.index ["university_id"], name: "index_users_on_university_id"
end
# Models
class University < ApplicationRecord
has_many :staffs
end
class User < ApplicationRecord
self.inheritance_column = :role
enum role: { Staff: 0, Clinician: 1 }
end
class Clinician < User
end
class Staff < User
belongs_to :university
end
Staff.first.university # => returns instance of University
Clinician.first.university # => raises NoMethodError
University.first.staffs # => returns collection of Staff objects
University.first.clinicians # => raises NoMethodError
Note that there is no type column. It's been overridden by the role:integer column used for the enum by setting self.inheritance_column = :role. You can interact with the enum roles with the string/symbol representation ("Staff", Staff.new, User.first.Staff?, User.first.Staff!, User.new(role: "Staff") and ActiveRecord takes care of converting that string to the right integer for the database queries.
For example, here's the query for User.where(role: "Staff")
SELECT "users".* FROM "users" WHERE "users"."role" = 0
Staff.all returns the same result but the wording of the query is slightly different
SELECT "users".* FROM "users" WHERE "users"."role" IN (0)
See this question for more detail: Same Model with different columns Rails
You can give a condition to the validates call in your User.rb model:
validates :university, presence: true, if: lambda { self.role.to_s == 'staff' }
# watch out for symbol vs. string in your self.role array
And I think (never done it but I guess that would work) you can do this for the :clinician role:
validates :university, presence: false, if: lambda { self.role.to_s == 'clinician' }
first question for me here! Im trying to assign 'key companies' to my users. These are found in a many-to-many rich join table. On this table there are attributes like new, key, active and so forth. I want to assign companies to users in a long list and for that Im using SimpleForm.
Everything is working excepts that I want to filter out and limit the association relation based on the attributes on the rich relation. I have company relations for each user but not all of them are akey-relation or a new-relation for example. I only want the association being key to show up and not touch the other ones. I also want to set the attribute active to true when Im assigning these companies to the users. My code looks like this now:
user.rb
class User < ActiveRecord::Base
has_many :company_user_relationships
has_many :companies, through: :company_user_relationships
company.rb
class Company < ActiveRecord::Base
has_many :company_user_relationships
has_many :users, through: :company_user_relationships
schema.rb
create_table "company_user_relationships", force: true do |t|
t.integer "company_id"
t.integer "user_id"
t.boolean "key"
t.boolean "active"
t.datetime "last_contacted"
t.string "status_comment"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "status"
t.boolean "new"
end
users_controller.rb
def assign_key_companies
User.update(params[:users].keys, params[:users].values)
redirect_to(:back)
end
view
= form_for :user_companies,
url: assign_key_companies_users_path,
html: {:method => :put} do |f|
- users.each do |user|
= simple_fields_for "users[]", user do |u|
tr
td.col-md-4
= "#{user.first_name} #{user.last_name}"
td.col-md-8
= u.association :companies, label: false, collection: #key_company_candidates,
input_html: {data: {placeholder: " Assign key companies"}, class: 'chosen-select'}
= submit_tag "Save key companies", class: "btn btn-success pull-right"
I basically want to only show user.companies.where(key: true) and the SQLCOMMIT to always put the key-field to true when updating the record.
How can i filter out to only affect the associations I want?
I can think of two ways.
First to filter it at the association level
class User < ActiveRecord::Base
has_many :company_user_relationships, -> { where("company_user_relationships.key" => true) }
Or a where
user.companies.where("company_user_relationships.key" => true)
When you call user.companies it actually doing the join table among all three tables, so you could specify the condition like my example.
Given the following form
class EntryForm < Reform::Form
property :composition
property :native_language_version
validates :composition, presence: true
end
and the following schema
create_table "entries", force: :cascade do |t|
t.text "composition"
t.text "native_language_version"
t.integer "language_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
and the following controller code
class EntriesController < ApplicationController
def new
#entry = EntryForm.new(Entry.new)
# #entry = Entry.new
end
end
and the following code for simple_form
= simple_form_for(#entry) do |f|
= f.input :composition
= f.input :native_language_version
= f.submit
rather than getting a textarea for composition and native_language_version, I get
<input class="string required form-control" type="text" name="entry[composition]" id="entry_composition">
changing to using #entry = Entry.new gives me a textarea element instead, which is what I want:
<textarea class="text optional form-control" name="entry[composition]" id="entry_composition"></textarea>
I tried adding type: :text to the :composition property in EntryForm, but it didn't help.
I also know that rather than using f.input I could specify the actual input type, but that's a hack.
How do I pass the fact that composition is a text rather than a string through EntryForm to simple_form?
I'm using Rails 4.2.5.1, simple_form 3.2.1, and reform 2.1.0.
You can't. When the model is wrapped by a Reform::Form, you have to explicitly tell SimpleForm that you want a textarea.
= f.input :composition, as: :text_area
The reason is that when determining column database type SimpleForm relies on a part of ActiveRecord interface which the Reform::Form doesn't provide.
In a Rails ( 4.1.5 / ruby 2.0.0p481 / win64 ) application I have a many-to-many relationship between Student and Course and a join model StudentCourse which represents the association, which has an additional attribute called "started", which is set by default on "false".
I also have added an index in the join table made of the student_id and the course_id, and set a unique check on that, like this
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
Now I see that associations are created by either doing:
Student.first.courses.create(:name => "english")
or
Course.first.students << Student.first
This is fine and it's the expected behaviour, I suppose.
What I am looking after is the correct way to get and set the "started" attribute.
I am seeing an odd behaviour when accessing that attribute from the other models and not straight from the join model.
s = Student.create
c = Course.create(:name => "english")
s.student_courses.first
=> | "english" | false | # (represented as a table for practicity)
s.student_courses.first.started = true
=> | "english" | true |
s.save
=> true
Ok this looks like it has been saved but when I loot ak:
StudentCourse.first
=> | 1 | 1 | false |
So it is set on true if I go through the student nested attributes, but it's still false in the join model. I also tried doing "reload!" but it makes no difference and they will mantaint their own different value.
If something is going so bad that values are not actually persisted I should be told instead of getting "true" when saving, because otherwise how bad could be the consequences of this ? What am I missing here?
Anyway, if I try modifying the "started" attribute on the join model directly, I meet another kind of problem:
StudentCourse.first.started = true
StudentCourse Load (1.0ms) SELECT "student_courses".* FROM "student_courses" LIMIT 1
=> true
StudentCourse.first.started
=> false
It has not changed!
StudentCourse.find_by(:student_id => "10", :course_id => "1").started = true
=> true
StudentCourse.find_by(:student_id => "10", :course_id => "1").started
=> false
Same as before.. I try with:
StudentCourse.find(1).started = true
ActiveRecord::UnknownPrimaryKey: Unknown primary key for table student_courses in model StudentCourse.
Then with:
sc = StudentCourse.first
sc.started = true
=> true
sc
=> | 1 | 1 | true |
seems great but when saving:
sc.save
(0.0ms) begin transaction
SQL (1.0ms) UPDATE "student_courses" SET "started" = ? WHERE
"student_courses"."" IS NULL [["started", "true"]]
SQLite3::SQLException: no such column: student_courses.: UPDATE
"student_courses" SET "started" = ? WHERE "student_courses"."" IS NULL
(1.0ms) rollback transaction ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such column: student_courses.: UPDATE
"student_courses" SET "started" = ? WHERE "student_courses"."" IS
NULL from
C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.9-x64-mingw32/lib/sqlite3/database.rb:91:in
`initialize'
So I think this all has to do with not having a primary key in
join-table?
But I am not sure enough on how to use it and if that'd represent a
good practice for the case I am trying to solve ?
Also, if this is the problem, why then I don't get the same warning
here when I save the student after I do
s.student_courses.first.started = true, as shown in the examples
above?
Code
student.rb
class Student < ActiveRecord::Base
has_many :student_courses
has_many :courses, :through => :student_courses
end
course.rb
class Course < ActiveRecord::Base
has_many :student_courses
has_many :students, :through => :student_courses
end
student_course.rb
class StudentCourse < ActiveRecord::Base
belongs_to :course
belongs_to :student
end
schema.rb
ActiveRecord::Schema.define(version: 20141020135702) do
create_table "student_courses", id: false, force: true do |t|
t.integer "course_id", null: false
t.integer "student_id", null: false
t.string "started", limit: 8, default: "pending", null: false
end
add_index "student_courses", ["course_id", "student_id"], name: "by_course_and_student", unique: true
create_table "courses", force: true do |t|
t.string "name", limit: 50, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "students", force: true do |t|
t.string "name", limit: 50, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
end
create_join_table.rb (migration for join table)
class CreateJoinTable < ActiveRecord::Migration
def change
create_join_table :courses, :students, table_name: :student_courses do |t|
t.index [:course_id, :student_id], :unique => true, :name => 'by_course_and_student'
t.boolean :started, :null => false, :default => false
end
end
end
Ok I finally got what was going on here:
If you create a join table in a migration using #create_join_table, this method will not create the default primary key called "id" (and not add an index for it) which is what rails does by default when using #create_table.
ActiveRecord needs a primary key to build its queries, because it is the column that it will be used by default when doing things like Model.find(3).
Also if you think you can get around this by doing something like StudentCourse.find_by(:course_id => "1", :student_id => "2").update_attributes(:started => true) [0] it will still fail, because after the record it's found, AR will still try to update it looking at the "id" of the record it found.
Also StudentCourse.find_by(:course_id => "1", :student_id => "2").started = true will retrun true but of course it is not saved until you call #save on it. If you assign it to a var relationship and then you call relationship.save you will see it will fail to save for the above reasons.
[0]
In the join table I didn't want duplicate records for a "student_id" and "course_id" so in the migration I had explicitely added a unique constraint for them (using unique index).
This led me to think that I did not need anymore a primary key to uniquely identify a record, because I had those two values... I thought that adding an index on them was enough for they to work as a primary key... but it is not. You need to explicitely define a primary-key when you are not using the default "id" one.
Also turns out that Rails does not support composite primary keys and so even if I wanted to add a primary key build on those two values (so making them primary-key and unique-index, like default rails "id" works) it would have not been possible.
A gem for that exists: https://github.com/composite-primary-keys/composite_primary_keys
So, end of the story, the way I fixed it was simply adding t.column :id, :primary_key to the migration for the join table creation. Also I could have not created the join table with #create_join_table but instead using just #create_table (which would create an "id" automatically").
Hope this helps someone else.
Also this answer to another question was very helpful, thank you #Peter Alfvin !
OK, it appears that you don't have a primary key (we are getting confirmation shortly) in your join table. You do need to have a primary key when trying to access the join table.
I would suggest your migration be:
class CreateStudentCourses < ActiveRecord::Migration
def change
create_table :student_courses do |t|
t.references :course
t.references :student
t.boolean :started, default: false
t.timestamps
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
end
end
end
The model definitions look good, so that would be the only change I can see that needs to be made.
After that, doing what you have been doing should work correctly. You would create the join and then access it after the creation. If you want to assign the boolean to true upon creation, you would need to create the record through the StudentCourse model with the information you need (student_id, course_id and started = true) instead of through either association.
StudentCourse.create(course_id: course.id, student_id: student.id, started: true)
s = Student.create
c = Course.create(:name => "english")
s.student_courses.first.started = true
s.save
I think the clue here is in the first line that you posted (represented above). s is an instance of the student and when you call s.save then you're asking the student to save any changes to its attributes. There are not any changes to save, however, because you made a change to an association.
You have a couple of options. If you prefer the direct access approach from your code snippet then the following should work.
s = Student.create
c = Course.create(:name => 'english')
s.courses << c
s.student_courses.first.update_attributes(:started => true)
Another alternative would be to use the accepts_nested_attributes_for macro to expose the started attribute from the student perspective.
class Student
has_many :student_courses, :inverse_of => :student
has_many :courses, :through => :student_courses
accepts_nested_attributes_for :student_courses
end
s = Student.create
c = Course.create(:name => 'english')
s.courses << c
s.update_attributes(:student_courses_attributes=>[{:id => 1, :started => true}])
Basically, all I need is a template_id field in my business table to be assigned correctly so that if I did Business.first.template it would return the result of the current assigned template for that business. At the moment I get I am just getting 'nil'
In my project a Business belongs_to a template (I believe this puts the primary key in the template table and the foreign key in the business table).
class Business < ActiveRecord::Base
belongs_to :template
class Template < ActiveRecord::Base
has_many :businesses
When the user fills out a form for a 'new business' they select the template they wish to use. The templates table is already filled with 3 templates, template_id, 0, 1, 2 (so I cant really work out if anything needs to be 'created'). The user is limited through the form to select only one of 3 templates (radio buttons).
When submitting the form and creating the business the link between the business and the template is currently not created. I don't have anything about the template creation in my business class because I cant work out what would need to be created, the template records already exist in the template table and are static.
Business Controller
def new
#business = current_user.businesses.build
#business.addresses.build
end
# POST /businesses
def create
#business = Business.new(business_params)
#business.users << current_user
if #business.save
redirect_to #business, notice: 'Business was successfully created.'
else
render action: 'new'
end
end
def business_params
params.require(:business).permit(:name, :email, :template_id, addresses_attributes [:number, :street, :suburb, :state, :country], template_attributes: [:name])
I am not sure if I should be assigning template_id myself or doing something with 'build_template'
Schema
create_table "businesses", force: true do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
t.string "description"
t.string "sub_heading"
t.string "email"
t.integer "template_id"
end
create_table "templates", force: true do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
t.integer "cost"
end
I am not sure if I should be assigning the value as either 0, 1 or 2 directly from the form submitted by the user to template_id in the business table or if I should be allowing nested attributes as I did with the addresses table.
Any help is appreciated.
Thanks
The foreign key to template id will be fine though. It is what ties an instance of Business to a and instance of Template.
You aren't creating a template, you are selecting one already from a list of created templates. You can access a business's template should be as simple as Business.find(id).template where Id is the id of the business you want knowledge about.