In my Rails 4 app, I have the following models:
class Post < ActiveRecord::Base
has_one :metadatum
end
class Metadatum < ActiveRecord::Base
belongs_to :post
end
Here is the schema of the Metadatum model:
create_table "metadata", force: :cascade do |t|
t.integer "post_id"
t.string "title"
t.text "description"
t.string "host"
t.string "image"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "link"
end
In the create action, I do the following to get data with the MetaInspector gem and store them in the Metadatum associated with the right Post record:
def create
[...]
if #post.format == "Link"
#metadatum = #post.build_metadatum
#link = MetaInspector.new(facebook_copy_link(#post.copy)) unless facebook_copy_link(#post.copy).blank?
if #link
#metadatum.title = #link.title
#metadatum.description = #link.meta_tags["name"]["description"].to_s.tr('[""]', '')
#metadatum.host = #link.host
#metadatum.image = #link.images.best
#metadatum.save
end
end
respond_to do |format|
if #post.save
[...]
end
end
end
Now, I need to implement a similar process in the edit action, taking into account that we have two cases:
The current Post :format was not set to "Link" prior to this edit, is now set to "Link", and therefore the Metadatum record associated with the Post is empty.
The current Post format was already set to "Link" prior to this edit, and we simply need to update the attributes of the Metadatum record associated with the Post.
Based on this reasoning, I have implemented the following in Posts#Update:
def update
[...]
if #post.format == "Link"
if #post.metadatum
#metadatum = #post.metadatum
else
#metadatum = #post.build_metadatum
end
#link = MetaInspector.new(facebook_copy_link(#post.copy)) unless facebook_copy_link(#post.copy).blank?
if #link
#metadatum.update!(title: #link.title,
description: #link.meta_tags["name"]["description"].to_s.tr('[""]', ''),
host: #link.host,
image: #link.images.best)
end
end
respond_to do |format|
if #post.update(post_params)
[...]
end
end
end
end
This only partially works and I can't understand why:
let's say I have Link A saved in the post, change it to Link B and update the post, then nothing changes at all, neither in the console nor in the Posts show view.
but, if I try and edit the same post again, keep Link B and update the post, then all right data appears, both in the console and in the Posts show view.
Any idea what is wrong here and how to fix it?
Related
SOLVED Please read more at the end of the question and in my answer below.
The accepted question solved the main error.
My own answer gives a description of how to solve the details.
Original Question:
I am trying to get a many-to-many relationship to work. I have been reading a lot on stack exchange and in the manual and tutorials for Rails. Until today I thought I had understood what to do, but now I need to retrieve information along the whole chain of models and nothing comes up from the database.
The relations are user <-> membership <-> usergroup <-> accessright <-> function, with membership and accessright as "through"-models.
class User < ActiveRecord::Base
has_many :memberships
has_many :usergroups, :through => :memberships
has_many :functions, :through => :usergroups
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :usergroup
end
class Usergroup < ActiveRecord::Base
has_many :accessrights
has_many :functions, :through => :accessrights
has_many :memberships
has_many :users, :through => :memberships
end
class Accessright < ActiveRecord::Base
belongs_to :function
belongs_to :usergroup
end
class Function < ActiveRecord::Base
has_many :accessrights
has_many :usergroups, :through => :accessrights
end
I have a user object in my grasp and I want to get the function names for this user object in a list. I try this in a helper module:
# Loads the functions the current user via her usergroup has access right to (if any).
def load_user_functions
if #current_user_functions.nil?
#current_user_functions = []
if logged_in?
#current_user.functions.each do |f|
#current_user_functions << f.name
end
end
end
end
From reading the manuals I got the impression that, if I set up the model correctly, I might do something like #current_user.functions.each implicitly.
I put a debugger on the website. I shows me the current user after login, but no information for the functions. They stay empty after login:
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
# reset function access rights
#current_user_functions = nil
load_user_functions
end
# Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
Here, the database schema for the tables, as created by my migrations:
create_table "accessrights", id: false, force: true do |t|
t.integer "usergroup_id"
t.integer "function_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "accessrights", ["usergroup_id", "function_id"], name: "index_accessrights_on_function_id_and_usergroup_id", unique: true, using: :btree
create_table "functions", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "memberships", id: false, force: true do |t|
t.integer "user_id"
t.integer "usergroup_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "memberships", ["user_id", "usergroup_id"], name: "index_memberships_on_user_id_and_usergroup_id", unique: true, using: :btree
create_table "usergroups", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", force: true do |t|
t.string "username"
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
t.string "password_digest"
end
This is the debugger, that I check the content of the variables with. Maybe the debugger can not handle lists like this?
<%= debug(#current_user) if Rails.env.development? %>
<%= debug(#current_user_functions) if Rails.env.development? %>
In case there is a meaning to it, the second debugger displays:
---
...
An extract of my database seed:
#function3 = Function.create!(name: "ViewCreateEditDeleteUsers")
#usergroup3 = Usergroup.create!(name: "Admin") # gets right to view, create, edit and delete user groups and users
Accessright.create!(function_id: #function3.id,
usergroup_id: #usergroup3.id)
#user =
User.create!(username: "AOphagen",
email: "ophagen#test.de",
password: "testing", password_confirmation: "testing", )
Membership.create!(user_id: #user.id,
usergroup_id: #usergroup3.id)
After a helpful suggestion by BroiSatse, I tried the rails console (ouch, should be my favourite tool) and the result is, I have set up the DB correctly:
User.find_by(username:"AOphagen").functions.first.name
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."username" = 'AOphagen' LIMIT 1
Function Load (2.1ms) SELECT "functions".* FROM "functions" INNER JOIN "accessrights" ON "functions"."id" = "accessrights"."function_id" INNER JOIN "usergroups" ON "accessrights"."usergroup_id" = "usergroups"."id" INNER JOIN "memberships" ON "usergroups"."id" = "memberships"."usergroup_id" WHERE "memberships"."user_id" = $1 ORDER BY "functions"."id" ASC LIMIT 1 [["user_id", 1]]
=> "ViewCreateEditDeleteUsers"
SOLVED The main error was in the database setup. This was solved with the answer I selected as solving my problem (thanks again!). The answer was concise, correct and helpful.
The details of the problem took more time to solve, so I give a detailed answer with how-to below.
I would love to read why my original idea (I knew it was not Ruby-like) did not work - and why the lazy fetch now does, please!
For each method you want to call on user you have to create a method. In this case you should use nested has_many :through like this:
class User < ActiveRecord::Base
has_many :memberships
has_many :usergroups, through: :memberships
has_many :functions, through: :usergroups
end
The tiny mean detail problem was my attempt to call a load functions snippet in the log_in(user) functions code (see above in my question).
Now I just set the users functions list to nil after login:
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
# reset function access rights
#current_user_functions = nil
end
Like with the #current_user I wait, until I need the functions for the first time, then load them, if they are still nil:
# Returns the currently logged-in users function names as list (if any).
def current_user_functions
if #current_user_functions.nil?
#current_user_functions = []
if logged_in?
#current_user.functions.each do |f|
#current_user_functions << f.name
end
end
end
end
(I have renamed the function accordingly.)
In the HTML I have a branch asking for a certain right:
<% if hasRightToViewCreateEditDeleteUsers? %>
<li><%= link_to "Users", '#' %></li>
<% end %>
(Still a dummy link there.)
I presume it is this that calls the code to load the function names...
def hasRightToViewCreateEditDeleteUsers?
current_user_functions.include?("ViewCreateEditDeleteUsers")
end
The debugger now shows the list of function names:
---
- ViewCreateEditDeleteUsers
And even though my if-clause in the HTML is not showing any link yet,
I know I will get this to work soon
that's irrelevant, since it was not my question in the first place
Hooray! Thank you every helpful person here! I will accept BroiSatse answer, because he taught me about my main mistake, then helped me debug the details.
I have two models: Draft and Pick. I want the Draft's ActiveRecord column current_pick to increase by 1 after a Pick is created.
Inside Pick, I have a method that increase draft.current_pick by 1:
class Pick < ActiveRecord::Base
belongs_to :draft
after_save :advance_draft
def advance_draft
draft.updraft
end
Inside draft, updraft is:
def updraft
self.current_pick += 1
end
My test to ensure the current_pick is being increased by one is:
it 'should run the advance_draft method after creating' do
team1 = FactoryGirl.create(:team)
team2 = FactoryGirl.create(:team_two)
cam = FactoryGirl.create(:player)
draft = FactoryGirl.create(:two_team_draft)
pick = Pick.create(:player_id => cam.id, :team_id => draft.team_at(draft.current_pick).id, :draft_id => draft.id, :draft_position => draft.current_pick)
draft.draft_position.should eq 2
end
The pick and draft are being created in the test but the updraft method is not being called on the correct draft because the pick.draft.draft_position remains at 1 after the pick has been created. (it should increase to 2).
Here is my schema:
create_table "drafts", force: true do |t|
t.integer "draft_position"
t.integer "number_of_teams"
t.integer "PPTD"
t.integer "PPR"
t.integer "current_pick", default: 1
end
create_table "picks", force: true do |t|
t.integer "player_id"
t.integer "team_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "draft_id"
t.integer "draft_position"
end
My question is, how do I properly increase the pick.draft.current_pick inside my test?
I would do 2 things, use increment! when updating the count to increment the value and save the record, and reload the object you're looking at, since its database representation has changed since it was created.
def updraft
increment!(:current_pick)
end
This will update the current_pick and save the object in one shot.
it 'should run the advance_draft method after creating' do
# ... setup
draft.reload.draft_position.should eq 2
end
Now, instead of using the instance of draft that was created before the modification occurred, it's using a version current with the database.
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.
I want to implement an opening time mode for a store.
currently i have
Class Store
has_one :opening_table
#return opening_times as an array
def opening_times
self.build_opening_table unless opening_table
(0..6).collect{ |i|
[opening_table.read_attribute("open_#{i}"),opening_table.read_attribute("close_#{i}") ]
}
end
def opening_times=(attr)
self.build_opening_table unless opening_table
i=0
attr.each do |el|
opening_table.attributes= {"open_#{i}".to_sym => el[0]}
opening_table.attributes= {"close_#{i}".to_sym => el[1]}
i=i+1
end
end
and OpeningTable has fields open_0, close_0 for monday opening and closing time
create_table :opening_tables do |t|
t.references :advertisement, :null=>false
(0..6).each do |i|
t.integer "open_#{i}"
t.integer "close_#{i}"
end
t.timestamps
end
i dont feel comfortable and flexible with this solution but i dont have an idea to implement it in a better way
You could make another table called Schedule
create_table :schedules do |t|
t.integer 'open'
t.integer 'close'
t.integer store_id
t.timestamps
end
And then make the store has _many :schedules
So you can acces Store.first.schedules[0].open And so on.
EDIT:
For the nested attribute, you would simply access through the store.schedules[0]
or you could add to the schedules table:
t.string :week_day
and then do store.schedules.find_by_week_day("monday")
As for the validation, add that to your models/store.rb
validate :has_seven_schedules
...
def has_seven_schedules
self.schedules.count == 7 ? true : false
end
EDIT2:
If you wanted you could put that find_by_week_day("monday") on a method inside the store.rb model like:
def opening_time(day)
schedule = self.schedules.find_by_week_day(day)
unless schedule.nil?
return schedule.open
end
end
And the same for the closing_time.
I'm new to Rails. I'm building an app that has a user model and a profile model.
I want to associate these models such that:
- After the user creates an account, he is automatically sent to the "create profile" page, and the profile he creates is connected to only that particular user.
- Only the user who owns the profile can edit it.
I generated the user model using nifty_generators. When the user hits submit for the account creation, I redirect him to the "new profile" view to create a profile. I did this by editing the redirect path in the user controller. The user controller looks like this:
def create
#user = User.new(params[:user])
if #user.save
session[:user_id] = #user.id
flash[:notice] = "Thank you for signing up! You are now logged in."
redirect_to new_profile_path
else
render :action => 'new'
end
end
This was working, but the problem was that the app didn't seem to recognize that the profile was connected to that particular user. I was able to create profiles, but there didn't seem to be a relationship between the profile and the user.
My Profile model lists: belongs_to :user
My User model lists: has _one :profile
My routes.rb file lists the following:
map.resources :users, :has_one => :profile
map.resources :profiles
I have a user_id foreign key in the profiles table. My schema looks like this:
create_table "profiles", :force => true do |t|
t.integer "user_id"
t.string "name"
t.string "address1"
t.string "address2"
t.string "city"
t.string "state"
t.string "zip"
t.string "phone"
t.string "email"
t.string "website"
t.text "description"
t.string "category"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", :force => true do |t|
t.string "username"
t.string "email"
t.string "password_hash"
t.string "password_salt"
t.datetime "created_at"
t.datetime "updated_at"
end
To try to connect the profile to the user, I updated the profiles_controller.rb file with the following, which I basically extrapolated from the Rails Getting Started Guide. My thinking is that in my app, profiles connect to users in the same way that in the Rails Getting Started app, comments connect to posts. Here's the relevant parts of my profiles controller. I can provide the whole thing if it will help:
def new
#user = User.find(params[:user_id])
#profile = #user.profile.build
end
def create
#user = User.find(params[:user_id])
#profile = #user.profile.build(params[:profile])
if #profile.save
flash[:notice] = 'Profile was successfully created.'
redirect_to(#profile)
else
flash[:notice] = 'Error. Something went wrong.'
render :action => "new"
end
end
After making these updates to the profiles controller, now when I submit on the account creation screen, I'm redirected to an error page that says:
ActiveRecord::RecordNotFound in ProfilesController#new
Couldn't find User without an ID
This all seems like a pretty straight-forward Rails use case, but I'm not sure which pieces are wrong. Thanks in advance for your help!
When a user is created, the client is redirected to the new action in the ProfileController without an id. You need to explictly pass the user's id in the parameters. It's an idiom to pass the reference to the entire object, not just the id.
# app/controllers/users_controller.rb
def create
# ...
redirect_to new_user_profile_path(:user_id => #user)
# ...
end
Notice that I'm using new_user_profile_path and not new_profile_path. The former is the nested resource that you defined in routes.rb. You should delete map.resources :profiles because a profile cannot exist without a user.
For usability sake the profile should be part of the user model, or you should create your account and at least the base of your profile all in one go, as a user I i registered and then had to fill in another form I think I would be pretty annoyed.
The upside of this is that either way you can do this with one form. I'd look at the railscasts on complex forms and if you can handle a very modest financial outlay then the pragmatic programmers series on Mastering Rails Forms is a total winner.
Because user_id doesn't exist or is not created in the profile database. When creating user