Phoenix/Ecto - association not working - phoenix-framework

Working through a sample guide. What's detailed in the chapter doesn't work in my app. Pretty simple stuff, it would seem. I've got a Video model:
defmodule Rumbl.Video do
use Rumbl.Web, :model
schema "videos" do
field :url, :string
field :title, :string
field :description, :string
belongs_to :user, Rumbl.User
belongs_to :category, Rumbl.Category
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:url, :title, :description])
|> validate_required([:url, :title])
|> assoc_constraint(:category)
end
end
I've also got a Category model:
defmodule Rumbl.Category do
use Rumbl.Web, :model
schema "categories" do
field :name, :string
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name])
|> validate_required([:name])
end
def alphabetical(query) do
from c in query, order_by: c.name
end
def names_and_ids(query) do
from c in query, select: {c.name, c.id}
end
end
In an IEX session, I load a Video record as so:
iex(21)> video = Repo.one(from v in Video, limit: 1)
[debug] QUERY OK source="videos" db=16.0ms
SELECT v0."id", v0."url", v0."title", v0."description", v0."user_id", v0."category_id", v0."inserted_at", v0."updated_at" FROM "videos" AS v0 LIMIT 1 []
%Rumbl.Video{__meta__: #Ecto.Schema.Metadata<:loaded, "videos">,
category: #Ecto.Association.NotLoaded<association :category is not loaded>,
category_id: nil, description: "test1", id: 2,
inserted_at: #Ecto.DateTime<2017-01-02 06:50:26>, title: "test1",
updated_at: #Ecto.DateTime<2017-01-02 06:50:26>, url: "test1 video.com",
user: #Ecto.Association.NotLoaded<association :user is not loaded>,
user_id: 10}
I get why the category and user associations are not loaded. I didn't preload the user and there isn't a category association yet to load.
Either way, I've got my video in memory:
iex(22)> v.id
2
Now I load my category:
iex(23)> category = Repo.get_by Category, name: "Comedy"
[debug] QUERY OK source="categories" db=0.0ms
SELECT c0."id", c0."name", c0."inserted_at", c0."updated_at" FROM "categories" AS c0 WHERE (c0."name" = $1) ["Comedy"]
%Rumbl.Category{__meta__: #Ecto.Schema.Metadata<:loaded, "categories">, id: 4,
inserted_at: #Ecto.DateTime<2017-01-07 07:03:00>, name: "Comedy",
updated_at: #Ecto.DateTime<2017-01-07 07:03:00>}
Just to prove that I have it:
iex(24)> category.id
4
Now I try to associate the video with the category:
iex(25)> changeset = Video.changeset(video, %{category_id: category.id})
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Rumbl.Video<>,
valid?: true>
iex(26)> Repo.update(changeset)
{:ok,
%Rumbl.Video{__meta__: #Ecto.Schema.Metadata<:loaded, "videos">,
category: #Ecto.Association.NotLoaded<association :category is not loaded>,
category_id: nil, description: "test1", id: 2,
inserted_at: #Ecto.DateTime<2017-01-02 06:50:26>, title: "test1",
updated_at: #Ecto.DateTime<2017-01-02 06:50:26>, url: "test1 video.com",
user: #Ecto.Association.NotLoaded<association :user is not loaded>,
user_id: 10}}
I don't understand why there aren't any changes in the changeset. This is how the guide instructs to do an association. Am I missing something?
Thanks,
John

I figured it out. I needed to add the category_id to the list of params in the Video model:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:url, :title, :description, :category_id])
|> validate_required([:url, :title])
|> assoc_constraint(:category)
end

Related

Ruby function to get specific item in array and dynamically get nested data

If I have an array of hashes that looks like this
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some street',
postcode: '98877',
#...
}
}, {
#...
}]
can you write a function to get a specific item in an array, iterate over it and dynamically retrieve subsequently nested data?
This example doesn't work, but hopefully better explains my question:
def getDataFromFirstItem(someVal)
array(0).each{ |k, v| v["#{ someVal }"] }
end
puts getDataFromFirstItem('name')
# Expected output: 'Stan'
For context, I'm trying to create a Middleman helper so that I don't have to loop through a specific array that only has one item each time I use it in my template. The item (a hash) contains a load of global site variables. The data is coming from Contentful, within which everything is an array of entries.
Starting in ruby 2.3 and greater, you can use Array#dig and Hash#dig which both
Extracts the nested value specified by the sequence of idx objects by calling dig at each step, returning nil if any intermediate step is nil.
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some Street',
postcode: '98877'
}
}, {
}]
array.dig(0, :name) # => "Stan"
array.dig(0, :address, :postcode) # => "98877"
array.dig(0, :address, :city) # => nil
array.dig(1, :address, :postcode) # => nil
array.dig(2, :address, :postcode) # => nil
Please try this
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some street',
postcode: '98877',
#...
}
},
{
name: 'Nimish',
surname: 'Gupta',
address: {
street: 'Some street',
postcode: '98877',
#...
}
}
]
def getDataFromFirstItem(array, someVal)
array[0][someVal]
end
#Run this command
getDataFromFirstItem(array, :name) # => 'Stan'
#Please notice I send name as a symbol instead of string because the hash you declared consists of symbol keys
#Also if you want to make a dynamic program that works on all indexes of an array and not on a specific index then you can try this
def getDataFromItem(array, index, someVal)
if array[index]
array[index][someVal]
end
end
getDataFromItem(array, 0, :name) # => Stan
getDataFromItem(array, 1, :name) # => Nimish
getDataFromItem(array, 2, :name) # => nil
Hope this works, Please let me know if you still faces any issues

How to handle mongoid custom type dirty tracking?

I've got the following custom field type that allows me to save a GeoJSON LineString while treating the field like an array of points:
class GeoLineString
attr_reader :coordinates
def initialize(array)
#coordinates = array
end
# Converts an object of this instance into a database friendly value.
def mongoize
{
"type" => "LineString",
"coordinates" => #coordinates
}
end
def as_json(options={})
mongoize
end
class << self
# Get the object as it was stored in the database, and instantiate
# this custom class from it.
def demongoize(object)
return self.new(object["coordinates"]) if object.is_a?(Hash)
end
# Takes any possible object and converts it to how it would be
# stored in the database.
def mongoize(object)
case object
when GeoLineString then object.mongoize
when Array then GeoLineString.new(object).mongoize
else object
end
end
# Converts the object that was supplied to a criteria and converts it
# into a database friendly form.
def evolve(object)
case object
when GeoLineString then object.mongoize
else object
end
end
end
end
Which i use as follows in my model:
class Track
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
field :coordinates, type: ::GeoLineString
index({ coordinates: "2dsphere" }, { min: -200, max: 200 })
end
This works as expected but when i want to change the coordinates inside of the GeoLineString field mongoid does not recognize the field "coordinates" as dirty an thus does not update it in the database. Example:
t=Track.last
=> #<Track _id: 568a70e6859c862ee1000000, created_at: 2016-01-04 13:17:40 UTC, updated_at: 2016-01-04 13:17:40 UTC, name: nil, coordinates: {"type"=>"LineString", "coordinates"=>[[1, 2], [2, 2]]}>
t.coordinates.coordinates.push([3,3])
t
=> #<Track _id: 568a70e6859c862ee1000000, created_at: 2016-01-04 13:17:40 UTC, updated_at: 2016-01-04 13:17:40 UTC, name: nil, coordinates: {"type"=>"LineString", "coordinates"=>[[1, 2], [2, 2], [3, 3]]}>
t.changed?
=> false
How can i get mongoid to recognize that the value has changed?

ActiveSupport::Concern, has_secure password not update some columns

I have a Recoverable module for my Customer model. Customer model using has_secure method for authentication. Here is the Customer model:
class Customer < ActiveRecord::Base
include Recoverable
##
# Validations
validates :email, format: { with: REGEX_EMAIL }, allow_nil: false, allow_blank: false
validates_uniqueness_of :email
validates_presence_of :email
has_secure_password
validates :password, length: { minimum: 6 }, if: :password_digest_changed?
validates :password_confirmation, presence: true, if: :password_digest_changed?
end
And here is the Recoverable module:
# encoding: utf-8
module Recoverable
extend ActiveSupport::Concern
def reset_password!(new_password, new_password_confirmation)
self.password = new_password
self.password_confirmation = new_password_confirmation
if valid?
self.reset_password_token = nil
self.reset_password_sent_at = nil
end
save
end
end
My problem is after reset_password called reset_password_token, reset_password_sent_at are not null. It's not set to null. Update query is not set below columns. Why? Am I miss something? If you need more info let me know.
My environments: I'm using Rails 4 app.
UPDATE 1
When I puts self.inspect I get following outputs:
#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$U2knjpm5LF1V/sgXag0DcOpgZWHSpLw8nfCy4U8D57s6...", created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 10:04:45", reset_password_sent_at: nil, reset_password_token: nil>
UPDATE 2:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"PbUhgSPvQZWXflT5fA1WhqhHJX3c7NMapg6eeDQvpBI=", "token"=>"fiMXi2_4cYCHsFMop9TJBL2Qeqc41xWhHA", "q"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}}
Unpermitted parameters: utf8, _method, authenticity_token, q
Customer Load (0.4ms) SELECT "customers".* FROM "customers" WHERE "customers"."reset_password_token" IS NULL LIMIT 1
Unpermitted parameters: password_confirmation
Unpermitted parameters: password
Customer Exists (0.3ms) SELECT 1 AS one FROM "customers" WHERE ("customers"."email" = 'milk#yahoo.com' AND "customers"."id" != 79) LIMIT 1
----------------------------BEFORE:
#<ActiveModel::Errors:0xb593c280 #base=#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTK...", store_id: 124, created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 11:56:52", reset_password_sent_at: nil, reset_password_token: nil>, #messages={}>
(0.1ms) BEGIN
CACHE (0.0ms) SELECT 1 AS one FROM "customers" WHERE ("customers"."email" = 'milk#yahoo.com' AND "customers"."id" != 79) LIMIT 1
SQL (0.3ms) UPDATE "customers" SET "password_digest" = $1, "updated_at" = $2 WHERE "customers"."id" = 79 [["password_digest", "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTKUI001xu0O"], ["updated_at", Thu, 16 May 2013 19:58:25 ULAT +08:00]]
(16.5ms) COMMIT
---------------------------- SAVE:
true
----------------------------AFTER:
#<ActiveModel::Errors:0xb593c280 #base=#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTK...", store_id: 124, created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 11:58:25", reset_password_sent_at: nil, reset_password_token: nil>, #messages={}>
Ok so finally if your model is not valid after clearing variables you can do that:
save(validate: false)
It will skip validation and will allow you to save invalid model
Could you check if your model is really valid ?
I mean something like
if valid?
puts "valid"
self.reset_password_token = nil
self.reset_password_sent_at = nil
else
puts self.errors.inspect
end
Maybe you have some forgotten validation and you are not going to that block ?

Rails 3.0.9 : ActiveRecord Uniqueness Constraint failing on every updated, doesn't matter if the unique column isn't touched

I have a Profile model
class Profile < ActiveRecord::Base
attr_accessible :user_id, :race_id, :nickname, :first_name, :last_name, :gender, :birth_date, :eighteen,
:time_zone, :metric_scale, :referral_code, :referrer_id, :tag_line
# Relationships
belongs_to :user
belongs_to :race
belongs_to :referred_by, :class_name => "Profile", :foreign_key => "referral_code"
has_many :referrals, :class_name => "Profile", :foreign_key => "referrer_id"
# Validations
validates :user_id, :race_id, :nickname, :first_name, :last_name, :time_zone, :gender, :presence => true
validates :referral_code, :nickname, :uniqueness => { :case_sensitive => false }
# Instance Methods
def full_name
first_name + " " + last_name
end
# Class Methods
def self.search(search)
search_condition = "%" + search + "%"
find(:all, :conditions => ['nickname LIKE ?', search_condition])
end
def self.find_by_referral_code(referrer_code)
find(:one, :conditions => ['referral_code LIKE ?', referrer_code])
end
end
No matter which column I am updated the Uniqueness Constraint on 'referral_code' false and I cannot update the model and I can't figure out why. From what I read online as of Rails 3 ActiveRecord was supposed to be tracking dirty objects and only generating update queries containing the altered columns leaving all others alone. Because it should only be performing update queries on columns other than the Unique ones the validation should not be failing. Unfortunately it is. Here is Rails Console session displaying this:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > update = {"tag_line"=>"Changed to this"}
=> {"tag_line"=>"Changed to this"}
ruby-1.9.2-p180 :003 > profile.update_attributes(update)
=> false
ruby-1.9.2-p180 :004 > profile.errors
=> {:referral_code=>["has already been taken"]}
Even performing an update directly on a single column which is not unique causes the uniqueness constraint to fail and the record will not be updated, here is a console session:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > profile.tag_line = "changed to this"
=> "changed to this"
ruby-1.9.2-p180 :003 > profile.save
=> false
ruby-1.9.2-p180 :004 > profile.errors
=> {:referral_code=>["has already been taken"]}
I also ran a check to see if ActiveRecord was actually tracking the dirty object and it appears to be, here is the console session:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > profile.tag_line = "change to this"
=> "change to this"
ruby-1.9.2-p180 :003 > profile.changes
=> {"tag_line"=>["This is what its like.", "change to this"]}
ruby-1.9.2-p180 :004 > profile.save
=> false
ruby-1.9.2-p180 :005 > profile.errors
=> {:referral_code=>["has already been taken"]}
I honestly am at a loss, I have spent quite a bit of time digging into it as well as searching Google and I cannot find an answer as to why this is happening.
You are right, Rails does only "track" the dirty columns and generates the minimum update statement necessary. If you look in your log/development.log file you will see the actual SQL that is being generated, and you'll see that the update statement is only touching the columns you have edited. At least you would if your code was getting that far.
Before saving your model, Rails will run all the validations on it, and that includes seeing if the referral code is unique. To do this it will run a select SQL statement against the database to check; if you look in the development.log file you will definitely see this query.
So Rails is working correctly here.
If your referral codes are supposed to be unique, why aren't they? My guess would be that you are trying to save models with a nil or blank code. If that is the case, try adding :allow_nil => true or :allow_blank => true to the :uniqueness hash.

Nested Attributes not updating

With the following models:
class Location < ActiveRecord::Base
has_many :group_locations
has_many :groups, :through => :group_locations
accepts_nested_attributes_for :group_locations
end
class GroupLocation < ActiveRecord::Base
belongs_to :group
belongs_to :location
end
class Group < ActiveRecord::Base
has_many :group_locations
has_many :locations, :through => :group_locations
end
the following commands in rails console does not update the associated records:
>> l = Location.find(1)
=> #<Location id: 1, phone: "(949) 788-9999", ... created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-07 23:57:32">
\>\> l.group_locations
=> [#<GroupLocation group_id: 4, location_id: 1, created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-02 00:58:07">, #<GroupLocation group_id: **37**, location_id: 1,
created_at: "2011-06-02 00:58:07", updated_at: "2011-06-02 00:58:07">]
>> l.update_attributes(:phone => "(949) 788-9998", :group_locations_attributes =>
[{:group_id => 4, :location_id => 1}, {:group_id => **38**, :location_id => 1}])
=> true
>> l
=> #<Location id: 1, phone: "(949) 788-9998", ... created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-08 02:05:00">
>> l.group_locations
=> [#<GroupLocation group_id: 4, location_id: 1, created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-02 00:58:07">, #<GroupLocation group_id: **37**, location_id: 1,
created_at: "2011-06-02 00:58:07", updated_at: "2011-06-02 00:58:07">]
Note that the update_attributes call attempts to change the second GroupLocation to have group_id = 38, but the change is not made (even though the phone number did change). After looking at the code generated when this was implemented in the controller and view, changing the array to a hash (which is what is created in that case) has no different results (and the form/controller) have the same effect of not updating the associated records even though the main record is updated.
Any idea what I need to do to get the nested attributes to update?
From the logs you've displayed, it doesn't appear that your GroupLocation model has an :id primary key on it. While the join table for a HABTM has just the foreign keys (group_id, location_id) on it, the model used for a has_many :through association does need a primary key as well, :id by default. Otherwise, there is no way to determine which of the child objects to update in the case of an update.
Think of it this way - you are creating your association through another discrete model that should be able to stand entirely on its own.
The convention for nested attributes is if the hash passed to the nested_attributes includes an :id, then it is considered an update, if it doesn't then it's considered a create. In your case, you're not passing in an :id, so you get new GroupLocation records where you just wanted to update existing.
I believe, also, that once you have this in place correctly, you will be able to get rid of the attr_accessible, I don't think that should be necessary.
For good info on the nested attributes functionality that covers most of this, check out this page.
The actual answer is that the nested attributes must be accessible via attr_accessible. "accepts_nested_attributes" will only do what I want if it is accompanied by "attr_accessible :group_locations".

Resources