OK, so it's been years since I've written any ruby code, and my design may be incorrect. With that in mind, I'm writing a small utility to clone project entities in TargetProcess via REST. Target Process has a data model that allows for several types of parent:child relationships:
project:epic:feature:user_story
project:feature:user_story
project:user_story
However, all the entities are nearly identical from a data structure perspective, so it seemed to make sense to use STI and use models to define the relationships and inheritance. I've created a new Rails app with only these models to verify the error I'm getting when I attempt to associate an Epic with a Feature:
ActiveModel::MissingAttributeError: can't write unknown attribute `epic_id`
Here are the models:
class TargetProcessEntity < ActiveRecord::Base
end
class Project < TargetProcessEntity
has_many :epics
has_many :features
has_many :user_stories
end
class Project < TargetProcessEntity
has_many :epics
has_many :features
end
class Epic < TargetProcessEntity
belongs_to :project
has_many :features
end
class Feature < TargetProcessEntity
belongs_to :project
belongs_to :epic
has_many :user_stories
end
class UserStory < TargetProcessEntity
belongs_to :feature
belongs_to :project
end
Here is the schema:
ActiveRecord::Schema.define(version: 20150929122254) do
create_table "epics", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "target_process_entity_id"
t.integer "project_id"
end
add_index "epics", ["project_id"], name: "index_epics_on_project_id"
add_index "epics", ["target_process_entity_id"], name: "index_epics_on_target_process_entity_id"
create_table "features", force: :cascade do |t|
t.integer "project_id"
t.integer "epic_id"
t.integer "target_process_entity_id"
end
add_index "features", ["epic_id"], name: "index_features_on_epic_id"
add_index "features", ["project_id"], name: "index_features_on_project_id"
add_index "features", ["target_process_entity_id"], name: "index_features_on_target_process_entity_id"
create_table "projects", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "target_process_entity_id"
end
add_index "projects", ["id"], name: "index_projects_on_id"
add_index "projects", ["target_process_entity_id"], name: "index_projects_on_target_process_entity_id"
create_table "target_process_entities", force: :cascade do |t|
t.string "type", null: false
t.string "name"
t.text "description"
t.integer "source_remote_id"
t.float "numeric_priority"
t.integer "owner"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "cloned_remote_id"
t.string "resource_type"
t.integer "project_id"
end
create_table "user_stories", force: :cascade do |t|
t.integer "project_id"
t.integer "feature_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "target_process_entity_id"
end
add_index "user_stories", ["feature_id"], name: "index_user_stories_on_feature_id"
add_index "user_stories", ["project_id"], name: "index_user_stories_on_project_id"
add_index "user_stories", ["target_process_entity_id"], name: "index_user_stories_on_target_process_entity_id"
end
While Epic and Feature both have a project_id, an instance of Feature does not have an epic_id attribute; attempting to assign an epic to feature blows up:
[20] pry(main)> epic = Epic.new
=> #<Epic:0x007fcab6c80590
id: nil,
type: "Epic",
name: nil,
description: nil,
source_remote_id: nil,
numeric_priority: nil,
owner: nil,
created_at: nil,
updated_at: nil,
cloned_remote_id: nil,
resource_type: "Epic",
project_id: nil>
[21] pry(main)> feature = Feature.new
=> #<Feature:0x007fcab6d3ba48
id: nil,
type: "Feature",
name: nil,
description: nil,
source_remote_id: nil,
numeric_priority: nil,
owner: nil,
created_at: nil,
updated_at: nil,
cloned_remote_id: nil,
resource_type: "Feature",
project_id: nil>
[22] pry(main)> epic.save
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "target_process_entities" ("type", "resource_type", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["type", "Epic"], ["resource_type", "Epic"], ["created_at", "2015-10-02 15:18:13.351578"], ["updated_at", "2015-10-02 15:18:13.351578"]]
(4.6ms) commit transaction
=> true
[23] pry(main)> feature.epic = epic
ActiveModel::MissingAttributeError: can't write unknown attribute `epic_id`
from /Users/kcallahan/.rbenv/versions/2.0.0-p647/lib/ruby/gems/2.0.0/gems/activerecord-4.2.4/lib/active_record/attribute.rb:138:in `with_value_from_database'
[24] pry(main)>
I realize it is extremely possible I am either doing something wrong or have made a poor design decision; any input is hugely appreciated as I've not been able to find anything on this and have been banging my head against it for days!
OK, I got it working almost by mistake! I added the xxx_id columns to the target_process_entities table. I assumed that the STI tables would be able to respond to the relationship definitions, though my understanding of the inner workings of STI and relationships are rusty and incomplete at best...
I may be wrong but it looks like your Feature table is a join table for many to many relationship between Project and Epic.
If that's the case, your models might look like this
class Project < TargetProcessEntity
has_many :features
has_many :epics, through: :features
end
class Epic < TargetProcessEntity
has_many :features
has_many :projects, through: :features
end
class Feature < TargetProcessEntity
belongs_to :project
belongs_to :epic
has_many :user_stories
end
the source is implied if you use the same name
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Related
In trying to run a setup rake tasks(to populate my dbase). It is telling me that questions must exist. When I do a count on 'Question.count' - it returns 4 - so I know questions exist.
What am I missing? typo? syntax? When I look at similiar SO, it shows as some sort of typo but I am not seeing it.
Here is the code that is not working:
def add_option_group
puts " * Add Option Groups...\n"
OptionGroup.transaction do
create_option_group(
option_group_name: "Always-Never",
question_id: Question.find_by(question_name: "Do you have an updated photo?")
)
create_option_group(
option_group_name: "Yes-No",
question_id: Question.find_by(question_name: "Do you have a bio saved (updated in last 12 months)?")
)
end
end
def create_option_group(options={ })
puts " * CREATE OptionGroup Section...\n"
option_group_attributes = {}
attributes = option_group_attributes.merge options
option_group = OptionGroup.create! attributes
option_group.save!
option_group
end
I am getting this error message:
Add Option Groups...
CREATE OptionGroup Section...
rake aborted!
ActiveRecord::RecordInvalid: Validation failed: Questions must exist
/Users/axxx/workspace/fresh-assess/lib/tasks/setup.rake:314:in create_option_group' /Users/axxx/workspace/fresh-assess/lib/tasks/setup.rake:298:in block in add_option_group'
/Users/axxx/workspace/fresh-assess/lib/tasks/setup.rake:297:in add_option_group' /Users/axxx/workspace/fresh-assess/lib/tasks/setup.rake:51:in create_sample_data!
Migration files:
***** migrations ******
class CreateOptionGroups < ActiveRecord::Migration[6.1]
def change
create_table :option_groups do |t|
t.bigint :question_id
t.text :option_group_name
t.timestamps
end
end
end
class CreateQuestions < ActiveRecord::Migration[6.1]
def change
create_table :questions do |t|
t.bigint :assessment_section_id
t.bigint :input_type_id
t.text :question_name
t.string :question_subtext
t.boolean :question_required_yn
t.boolean :answer_required_yn
t.boolean :allow_multiple_options_answers_yn
t.integer :dependent_question_id
t.integer :dependent_question_option_id
t.integer :dependent_answer_id
t.timestamps
end
end
end
Here is schema
t.bigint "question_id"
t.bigint "option_choice_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "questions", force: :cascade do |t|
t.bigint "assessment_section_id"
t.bigint "input_type_id"
t.text "question_name"
t.string "question_subtext"
t.boolean "question_required_yn"
t.boolean "answer_required_yn"
t.boolean "allow_multiple_options_answers_yn"
t.integer "dependent_question_id"
t.integer "dependent_question_option_id"
t.integer "dependent_answer_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
Add foreign key:
class ForeignMoreKeysToModels < ActiveRecord::Migration[6.1]
def change
add_foreign_key :option_groups, :questions, validate: false
add_foreign_key :option_choices, :option_groups, validate: false
end
end
In the model:
class Question < ApplicationRecord
has_one :assessment_section
belongs_to :assessment_section, optional: true
has_many :option_groups
end
class OptionGroup < ApplicationRecord
has_many :option_choices
belongs_to :questions
end
What am I missing?
thx.
Two issues:
In the OptionGroup class, it should say belongs_to :question.
Try adding .id to the question lookup:
create_option_group(
option_group_name: "Always-Never",
question_id: Question.find_by(question_name: "Do you have an updated photo?").id
)
I recently updated my Rails app from 4.0 to 4.1. Everything seems to work fine, except this one line in my Search Model that was working before.
Essentially, I want to search/find District_Resources by Tag Name and by the District_Resource Name.
**ex.**
If I search the word "Tutoring"
*I should get all District_Resources with the Resource_Tag "Tutoring"
*And all District Resources that include the word Tutoring in it's Name.
(i.e Tutoring Services)
For some reason, I keep getting this error:
(Mysql2::Error: Unknown column 'resource_tags.name' in 'where
clause': SELECT `district_resources`.* FROM `district_resources`
WHERE (resource_tags.name like '%Tutoring%' OR district_resources.name like '%Tutoring%')
ORDER BY `district_resources`.`name` ASC):
But that column does exist in the Resource_Tags table.
MODELS
class DistrictResource < ActiveRecord::Base
has_many :district_mappings, dependent: :destroy
has_many :resource_tags, through: :district_mappings
accepts_nested_attributes_for :resource_tags
end
class ResourceTag < ActiveRecord::Base
has_many :district_mappings, dependent: :destroy
has_many :district_resources, through: :district_mappings
end
class Search < ActiveRecord::Base
def district_resources
#district_resources ||= find_district_resources
end
def find_district_resources
district_resources = DistrictResource.order(:name)
district_resources = district_resources.includes(:resource_tags).where("resource_tags.name like :name OR district_resources.name like :name", {:name => "%#{name}%" })
district_resources
end
end
SCHEMA
create_table "district_resources", force: true do |t|
t.string "name"
t.string "description"
t.string "website"
t.string "phone"
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "district_mappings", force: true do |t|
t.integer "district_resource_id"
t.integer "resource_tag_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "resource_tags", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
You are referencing district_resources in where clause but this is not joined in query as you are eager loading resource_tags so here are two solutions for this
1.
district_resources = district_resources.joins(:resource_tags).where("resource_tags.name like :name OR district_resources.name like :name", {:name => "%#{name}%" })
2.
district_resources = district_resources.includes(:resource_tags).refereces(resource_tags).where("resource_tags.name like :name OR district_resources.name like :name", {:name => "%#{name}%" })
In both of these cases we are telling rails that we are using resource_tags table in where clause so join district_resources with it
Fixed it!!
district_resources = district_resources.includes(:resource_tags).where("resource_tags.name like :name OR district_resources.name like :name", {:name => "%#{name}%" }).references(:resource_tags)
I had to reference the Resource_Tags Model
I have an existing app with devise running. User and log in and out etc. Thought I would like to get localhost:3000/users/15 changed to localhost:3000/users/ruby-boy so I have installed https://github.com/norman/friendly_id
I have ran rails generate friendly_id but when I ran rails generate scaffold user name:string slug:string:uniq, it say I already got a user.rb file so ok I've added: rails g migration AddSlugToUser slug:string:uniq and rails g migration AddNameToUser name:string. So I've added those two columns to my users table.
In my controllers I've replaced:
User.find(params[:id])
with:
User.friendly.find(params[:id])
Then ran this in console (rails c)
User.find_each(&:save)
But User.find_each(&:save) gave:
(0.1ms) commit transaction
(0.0ms) begin transaction
=> nil
I think I've followed the docs "eye for eye". Have I missed anything as the links wont change.
PS: I have edited the user.rb per website on github.
My user.rb:
class User < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: :slugged
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
belongs_to :plan
has_one :profile
attr_accessor :stripe_card_token
def save_with_payment
if valid?
customer = Stripe::Customer.create(description: email, plan: plan_id, card: stripe_card_token)
self.stripe_customer_token = customer.id
save!
end
end
end
Schema for users table:
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "plan_id"
t.string "stripe_customer_token"
t.string "slug"
t.string "name"
end
Add :name to your sign up form and then validate for uniqueness in your User model
Class User
validates :name, uniqueness: true
This way same name can't be created more than once and you'll avoid avoid weird slugs.
I ran the command
rails g migration AddUser_idToComments User_id:string
and then I figured out that User_id should be an integer and so I ran
rails g migration AddUser_idToComments User_id:integer --force thinking that it would overwrite the initial command.
But now, I'm getting this error:
```
louismorin$ rake db:migrate
== 20140910155248 AddIndexToComments: migrating ===============================
-- add_column(:comments, :Index, :string)
-> 0.0069s
== 20140910155248 AddIndexToComments: migrated (0.0070s) ======================
== 20140910181022 AddUserIdToComments: migrating ==============================
-- add_column(:comments, :User_id, :integer)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
SQLite3::SQLException: duplicate column name: User_id: ALTER TABLE "comments" ADD "User_id" integer/Users/louismorin/code/CP299/db/migrate/20140910181022_add_user_id_to_comments.rb:3:in change'
ActiveRecord::StatementInvalid: SQLite3::SQLException: duplicate column name: User_id: ALTER TABLE "comments" ADD "User_id" integer
/Users/louismorin/code/CP299/db/migrate/20140910181022_add_user_id_to_comments.rb:3:inchange'
SQLite3::SQLException: duplicate column name: User_id
/Users/louismorin/code/CP299/db/migrate/20140910181022_add_user_id_to_comments.rb:3:in `change'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
```
Here's my schema.rb file
```
ActiveRecord::Schema.define(version: 20140910155210) do
create_table "comments", force: true do |t|
t.text "body"
t.integer "post_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "User_Id"
end
add_index "comments", ["post_id"], name: "index_comments_on_post_id"
create_table "posts", force: true do |t|
t.string "title"
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
t.integer "topic_id"
end
add_index "posts", ["topic_id"], name: "index_posts_on_topic_id"
add_index "posts", ["user_id"], name: "index_posts_on_user_id"
create_table "topics", force: true do |t|
t.string "name"
t.boolean "public", default: true
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.string "role"
t.string "avatar"
t.string "Image"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
```
Because of the error, your second migration -- changing the column type -- didn't run. If that migration was only intended to change that one column, we could then delete the file it generated and try again.
If you don't yet have any data in that column that you care about, that's pretty easy:
rails g migration ChangeTypeOfUserIdOnComments
This migration name isn't special. Might as well have been DoWhateverIWant
Then, edit the created migration's change method to something like this:
def change
remove_column :comments, :user_id
add_column :comments, :user_id, :integer
add_index :comments, :user_id
end
When you then run your un-run migrations rake db:migrate, it shouldn't trip up on the one which errored (because we deleted it), and then it should run this one, which removes the column and adds it back with the correct type.
If you DO have data you want to save, the procedure is more complicated. Your change method would have to grab the current user_id's for each comment, and then assign them to the new comments when we create the new column. The below should hopefully work:
def change
user_ids = {}
Comment.find_each{ |c| user_ids[c.id] = c.user_id.to_i }
remove_column :comments, :user_id
add_column :comments, :user_id, :integer
add_index :comments, :user_id
Comment.each{ |c| c.update_attribute(:user_id, user_ids[c.id])
end
Also note that the names of the migration in the command are generally all CamelCase or all snake_case. So: AddColumnUserIdToComments or add_column_user_id_to_comments. Naming as you did might cause problems.
** EDIT **
Modify a Column's Type in sqlite3
It appears that SQLite has NO good way to modify/drop a column. I'd suggest either:
Dropping and restarting a SQLITe table
Drop the table, drop the original user_id migration and the line in the new one about removing the old user_id column, and then create a new table with the new migrations. Should work fine if you don't care about your data
Switching to Postgres, which is the database Heroku uses.
It's probably a good idea (because you want your production and local databases to behave identically), but can be bug-prone to switch to.
Look here for guidance -- Change from SQLite to PostgreSQL in a fresh Rails project
I'm playing with my first has_many through relationship. I have three models. Here they are.
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :pools
has_many :games,
through: :pools
end
~
class Pool < ActiveRecord::Base
has_many :users
has_many :games,
through: :users
end
~
class Game < ActiveRecord::Base
belongs_to :user
belongs_to :pool
end
Should this set up allow me to do the following in IRB?
u=User.first
u.games
When I do that, I get the following error.
SystemStackError: stack level too deep
from /Users/ShiftedRec/.rvm/rubies/ruby-2.0.0-p353/lib/ruby/2.0.0/irb/workspace.rb:86
Maybe IRB bug!
I should be able to do u.games, and u.pools?
it would also be nice to do a pool.users and pool.games.
How would I have to change my model set up so I can get access to those methods..?
I've been reading around but the has_many through is kind of confusing.
Here is my schema as well
ActiveRecord::Schema.define(version: 20140408165647) do
create_table "games", force: true do |t|
t.integer "pool_id"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "games", ["pool_id", "user_id"], name: "index_games_on_pool_id_and_user_id", unique: true
create_table "pools", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id" #vestige of old approach
end
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
I appreciate the help!
You were getting the error because your associations are defined incorrectly. Rails goofed up and went in recursion.
Update your models Pool and User as below:
class Pool < ActiveRecord::Base
has_many :games
has_many :users,
through: :games
end
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :games
has_many :pools,
through: :games
end
You are trying to create M-M Relationship between User and Pool through Game.
Also, remove user_id from pools table as you already have association through join table games.
Also, since you are on rails console(not IRB) after making above changes, reload the rails environment in console using reload! command.
Read more about has-many-through-associations in Rails guides.