Elixir Phoenix update nested many-to-many association - database

I'm using Elixir & Phoenix. I have the schemas Venues.Team and Accounts.Job with a many-to-many relationship between them.
defmodule Runbook.Venues.Team do
use Ecto.Schema
import Ecto.Changeset
schema "teams" do
field :name, :string
belongs_to :venue, Runbook.Venues.Venue
many_to_many :employees, Runbook.Accounts.Job, join_through: "jobs_teams"
timestamps()
end
#doc false
def changeset(team, attrs) do
team
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
and
defmodule Runbook.Accounts.Job do
use Ecto.Schema
import Ecto.Changeset
schema "jobs" do
field :role, :string
belongs_to :user, Runbook.Accounts.User
belongs_to :venue, Runbook.Venues.Venue
many_to_many :teams, Runbook.Venues.Team, join_through: "jobs_teams"
timestamps()
end
#doc false
def changeset(job, attrs) do
job
|> cast(attrs, [:role])
|> validate_required([:role])
end
end
I have the default Venues.update_team/2 method:
def update_team(%Team{} = team, attrs) do
team
|> Team.changeset(attrs)
|> Repo.update()
end
I want to be able to include a jobs argument inside the attrs parameter when updating a Team. This should insert an association into the :employees field.
I can do this in the interactive elixir shell (from ElixirSchool docs)
team = Venues.get_team!(1)
team_changeset = Ecto.Changeset.change(team)
team_add_employees_changeset = team_changeset |> put_assoc(:employees, [job])
Repo.update!(team_add_employees_changeset)
But I'm not sure how to abstract this to the update_team method, which builds the changeset up without a database call.
When I try to do it:
%Team{}
|> Team.changeset(%{id: 1, name: "Floor"})
|> put_assoc(:employees, [job])
|> Repo.update()
I get an error complaining that the :employees association is not loaded:
** (Ecto.NoPrimaryKeyValueError) struct `%Runbook.Venues.Team{__meta__: #Ecto.Schema.Metadata<:built, "teams">, employees: #Ecto.Association.NotLoaded<association :employees is not
loaded>, id: nil, inserted_at: nil, name: nil, updated_at: nil, venue: #Ecto.Association.NotLoaded<association :venue is not loaded>, venue_id: nil}` is missing primary key value
(ecto) lib/ecto/repo/schema.ex:898: anonymous fn/3 in Ecto.Repo.Schema.add_pk_filter!/2
(elixir) lib/enum.ex:1948: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto) lib/ecto/repo/schema.ex:312: Ecto.Repo.Schema.do_update/4
I can sorta do it like this:
changeset = Team.changeset(%Team{}, %{id: 1, name: "Floor"})
team = Venues.get_team!(1) |> Repo.preload(:employees)
preloaded_changeset = %Ecto.Changeset{changeset | data: team}
preloaded_changeset |> put_assoc(:employees, [job]) |> Repo.update()
(untested, a cleaner version of the below)
%Ecto.Changeset{tc | data: Venues.get_team!(1) |> Repo.preload(:employees)} |> put_assoc(:employees, [job]) |> Repo.update()
What is the best/cleanest/most conventional way to do this?

You can update by using Ecto.Changeset.cast_assoc/3
def update_team(%Team{} = team, attrs) do
team
|> Repo.preload(:employees) # has to be preloaded to perform update
|> Team.changeset(attrs)
|> Ecto.Changeset.cast_assoc(:employees, with: &Job.changeset/2)
|> Repo.update
end
update form
<%= form_for #changeset, #action, fn f -> %>
<%= if #changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :name %>
<%= text_input f, :name %>
<%= error_tag f, :name %>
<div class="form-group">
<%= inputs_for f, :job, fn cf -> %>
<%= label cf, :job_param_1 %>
<%= text_input cf, :job_param_1 %>
<%= error_tag cf, :job_param_1 %>
<% end %>
</div>
<div class="form-group">
<%= inputs_for f, :job, fn cf -> %>
<%= label cf, :job_param_2 %>
<%= text_input cf, :job_param_2 %>
<%= error_tag cf, :job_param_2 %>
<% end %>
</div>
<div>
<%= submit "Save" %>
</div>
<% end %>

Related

It possible to show attributes from model to other attributes from model?

Hi i have a view with model attributes :
name: "P",
surname: "a",
sorters: ["name","surname"] // dynamical array ( parameter to show )
in template :
<% for(i=0 ;i<sorters.length(); i++ ){ %>
<h2><%= sorters[0] %></h2> // its <%= 'name' %> with quotes
<% } %>
as result i got
name,
surname
i need get
P,
a
As result i get values from Sorters[array] not model values:
Some examples
1.
name: "P",
surname: "a",
sorters: ["name"]
P
2.
name: "P",
surname: "a",
sorters: ["surname","name"]
a, P
With this code in template i dont have a values from models but string text from my array and my view instead of attributes from model show labels
Based on the fact that sorters returns a value which appears to be equal to another field in the model, you wish to dynamically return that field.
There's a few ways to do this, probably the best is to provide the variable option when creating the template (docs). See example below:
var model = new Backbone.Model({
name: "P",
surname: "a",
sorters: ["name","surname"]
});
var tmp = _.template($('#template').html(), {variable: 'data'});
$('#result').html(tmp(model.attributes));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<script id="template" type="text/template">
<% for(i=0; i < data.sorters.length; i++ ){ %>
<h2><%= data[data.sorters[i]] %></h2>
<% } %>
</script>
<div id="result"/>
Also, better to use each rather than a for loop:
<% _.each(data.sorters, function(sorter) { %>
<h2><%= data[sorter] %></h2>
<% }) %>

Rails 4 PostgreSQL Array Type Problems - undefined method `gsub' for Fixnum

I recently tried adding an Array type column in my Rails app which uses Postgres 9.3. I followed some online tutorials and added the following code into my project.
Migration
add_column :matches, :p1_characters, :string, array: true, default: []
add_column :matches, :p2_characters, :string, array: true, default: []
Now if I try to manually update a match's characters in the console like so:
> m = Match.first
> m.p1_characters = ["1","2","3"]
> m.save
# Works fine!
Everything saves correctly in the database as expected.
The problem lies in trying to update the p1_characters/p2_characters attributes in my rails app.
p1_edit_character_.html.erb
<%= simple_form_for(#match, method: :patch,
:url => p1_set_character_match_path(#match)) do |f| %>
# displays a form of check boxes where the user can select multiple characters.
<h1>Select P1 Character</h1>
<%= f.input :p1_characters, label: "Player 1 Character", as: :check_boxes,
collection: #characters %>
<%= f.button :submit, "Select Character", class: "btn-lg btn-primary" %>
<% end %>
I know that the params being passed to my controller is:
"match"=>{"p1_characters"=>["1", "2", "3", ""]}
The error I get is
undefined method `gsub' for 1:Fixnum
Below is my controller with the relevant methods and strong params
matches_controller.rb
def p1_edit_character
#match = Match.find(params[:id])
#characters = Game.find(#match.game_id).characters
end
def p1_set_character
#match = Match.find(params[:id])
if #match.update_attributes(match_params)
if #match.p1_characters != []
flash[:notice] = "P1 character set."
end
redirect_to matches_path
end
end
private
def match_params
params.require(:match).permit(:p1_score, :p2_score, :match_date,
:p1_accepted, :p2_accepted, {p1_characters: []}, {p2_characters: []},
:disputed, :finalized_date)
end
Please, any guidance would be so helpful.
Thanks.

Backbone JS Nested Models with Backbone Relational

I am trying to implement Backbone with backbone-relational.
The Claim model:
define(['underscore', 'backbone', 'backbone-relational', 'models/User'], function(_, Backbone, relational, User) {
var Claim = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasOne,
key: 'a_user',
relatedModel: User
}],
defaults: {
},
initialize: function() {
},
clear: function() {
this.destroy();
this.view.remove();
}
});
return Claim;
});
The User model is a duplicate but with no relations set.
Below is the collection object:
Object
_byCid: Object
_byId: Object
_callbacks: Object
currentPage: 1
firstPage: 1
information: Object
length: 3
models: Array[3]
0: Object
1: Object
_callbacks: Object
_deferProcessing: false
_escapedAttributes: Object
_isInitialized: true
_pending: Object
_permitsUsed: 0
_previousAttributes: Object
_queue: Object
_relations: Array[1]
_silent: Object
attributes: Object
_deleted: false
_new: false
a_user: Object
_callbacks: Object
_escapedAttributes: Object
_isInitialized: true
_pending: Object
_permitsUsed: 0
_previousAttributes: Object
_queue: Object
_relations: Array[0]
_silent: Object
attributes: Object
_deleted: false
_new: false
already_in_save: false
already_in_validation: false
coll_claims: Array[0]
coll_claims_partial: true
created_at: "2012-12-12 09:00:00"
email: "cloud.strife#test.com"
firstname: "Cloud"
id: 2
lastname: "Strife"
modified_at: "2012-12-12 09:00:00"
modified_columns: Array[0]
start_copy: false
title: "Mr"
validation_failures: Array[0]
virtual_columns: Array[0]
__proto__: Object
changed: Object
cid: "c4"
collection: undefined
id: 2
__proto__: Object
already_in_save: false
already_in_validation: false
created_at: "2012-12-12 09:00:00"
fulfilment: "bank"
id: 2
manual: 0
modified_at: "2012-12-12 09:00:00"
modified_columns: Array[0]
promotion_id: 1
purchase_id: 2
start_copy: false
status: "pending"
user_id: 2
validation_failures: Array[0]
virtual_columns: Array[0]
__proto__: Object
changed: Object
cid: "c3"
collection: Object
id: 2
__proto__: Object
2: Object
length: 3
So essentially there are 3 Claim models in the collection and each Claim model has a nested model of User at the attribute key a_user.
The template looks like:
<% _.each( claims, function( item ){ %>
<tr>
<td><%= item.get("id") %></td>
<td><%= item.get("promotion_id") %></td>
<td><%= item.get("a_user").get("firstname") %></td>
<td><%= item.get("purchase_id") %></td>
<td></td>
<td><%= item.get("status") %></td>
<td><%= item.get("created_at") %></td>
</tr>
<% }); %>
However this results in the error:
TypeError: 'null' is not an object (evaluating 'item.get("a_user").get')
If I take it back to just item.get("a_user") it displays outputs [object Object]
I am just learning Backbone so any pointers are appreciated.
You shouldn't be using get within the template -- Underscore templates work with JSON, not Backbone model objects (you pass the model to the template using template(model.toJSON())), so they should be evaluated using basic dot-notation:
<tr>
<td><%= item.id %></td>
<td><%= item.promotion_id %></td>
</tr>

mongoid polymorphic association error

I'm having some problems using mongoid-3.0.6 with polymorphic fields.
Using rails 3.2.8 and ruby 1.9.3
Using a normal polymorphic relation:
class Shirt
include Mongoid::Document
field :name, localize: true
belongs_to :itemizable, polymorphic: true
end
class Item
include Mongoid::Document
field :price, type: Float
field :quantity, type: Integer, :default => 1
has_one :product, as: :itemizable
accepts_nested_attributes_for :product
end
The same association is available through the metadata:
>> Item.reflect_on_association(:product)
#<Mongoid::Relations::Metadata
  autobuild: false,
  class_name: Product,
  cyclic: nil,
  dependent: nil,
  inverse_of: nil,
  key: _id,
  macro: has_one,
  name: product,
  order: nil,
  polymorphic: true,
  relation: Mongoid::Relations::Referenced::One,
  setter: product=,
  versioned: false>
>> item = Item.new
#<Item _id: 50606c1668ce87692e000003, _type: nil, created_at: nil, updated_at: nil, deleted_at: nil, price: nil, quantity: 1>
but when i run
>> item.product = Shirt.new or >> item.build_product
i got always the same error
NameError: uninitialized constant Product
Full stack error
Any thoughts?
Thanks in advance.
Solved
Found the motive
Need to add the class_name to the relation
has_one :product, as: :itemizable, class_name: "Toy"

Mongoid relation not being persisted

class Account
include Mongoid::Document
include Geocoder::Model::Mongoid
geocoded_by :zip
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
before_validation :pass_confirm
after_validation :geocode_check
before_create :assign_subs
field :email, type: String
field :type, type: String
field :zip, type: String
field :oldzip, type: String
field :coordinates, type: Array
field :latitude, type: Float
field :longitude, type: Float
auto_increment :num, collection: :account_nums
index :num, unique: true
has_many :submissions
mount_uploader :photo, PhotoUploader
def self.find_by_num(num)
Account.where(num: num).first
end
protected
def pass_confirm
self.password_confirmation ||= self.password
end
def geocode_check
if self.oldzip != self.zip
self.oldzip = self.zip
self.geocode
end
end
def assign_subs
binding.pry
Submission.where(email: self.email).each do |sub|
sub.zip = self.zip
self.submissions << sub
end
end
end
--
class Submission
include Mongoid::Document
include Mongoid::Search
include Mongoid::Timestamps::Created
include Geocoder::Model::Mongoid
geocoded_by :zip
before_validation :fix_rate
after_validation :geocode
search_in :message, tags: :name
field :email, type: String
field :rate, type: String
field :message, type: String
field :type, type: String
field :zip, type: String
field :coordinates, type: Array
field :latitude, type: Float
field :longitude, type: Float
auto_increment :num, collection: :submission_nums
index :num, unique: true
has_and_belongs_to_many :tags
belongs_to :account
mount_uploader :photo, PhotoUploader
protected
def fix_rate
self.rate = self.rate.sub(/[^\d]*/, '').sub(/(\d*).*/, '\1')
end
end
--
pry(#<Account>)> self.submissions << Submission.first
=> [#<Submission _id: 4e751df86066252059000054, _type: nil, created_at: 2011-09-17 22:23:52 UTC, _keywords: ["tfnwuaty"], email: "krisbltn#gmail.com", rate: "49", message: "tfnwuaty", type: "person", zip: nil, coordinates: nil, latitude: nil, longitude: nil, num: 1, tag_ids: [], account_id: BSON::ObjectId('4e751e0d6066252059000059'), photo: "lawrence.jpg">]
pry(#<Account>)> self.submissions
=> []
as you see above, when trying to add a child document it doesn't get saved. Any ideas as to what could be going on?
Also- this is a has_many / belongs_to relationship, and when I change it to has_and_belongs_to_many it seems to work fine.
My guess is that you've upgraded Mongoid, but haven't read the upgrading docs.
Add , :autosave => true to Account's relation with Submission.
class Account
include Mongoid::Document
has_many :submissions, :autosave => true
end
class Submission
include Mongoid::Document
belongs_to :account
end
Account.delete_all
Submission.delete_all
Submission.create
account = Account.new
account.submissions << Submission.first
account.save
Submission.first.account == account
This was also submitted as a GitHub issue. Tsk tsk.

Resources