Ruby / Datamapper create or update issue - immutable error - ruby

I'm have a user class that can optionally have a billing address. When I post a payment form, assuming the user has indicated they want to save their billing address details, I want to either create a new address record or update the original one.
I have tried many things but the closest I can get to working code is...
class User
include DataMapper::Resource
property :id, Serial
property :provider, String, :length => 100
property :identifier, String, :length => 100
property :username, String, :length => 100
property :remember_billing, Boolean
has 1, :billing_address
end
class BillingAddress
include DataMapper::Resource
property :first, String, :length => 20
property :surname, String, :length => 20
property :address1, String, :length => 50
property :address2, String, :length => 50
property :towncity, String, :length => 40
property :state, String, :length => 2
property :postcode, String, :length => 20
property :country, String, :length => 2
property :deleted_at, ParanoidDateTime
belongs_to :user, :key => true
end
post "/pay" do
#post = params[:post]
#addr = params[:addr]
if #addr == nil
#addr = Hash.new
end
user = User.first(:identifier => session["vya.user"])
user.remember_billing = !!#post["remember"]
if user.remember_billing
user.billing_address = BillingAddress.first_or_create({ :user => user }, #addr)
end
user.save
...
which works fine when there is no record. But if there is already a record, it keeps the original values.
I saw a similar post
DataMapper: Create new record or update existing
but if I alter the code to be
user.billing_address = BillingAddress.first_or_create(:user => user).update(#addr)
I get the error
DataMapper::ImmutableError at /pay
Immutable resource cannot be modified
Any help much appreciated

You're chaining lots of things together, there. How about:
billing = BillingAddress.first_or_new(:user => user, #addr) #don't update, send the hash as second parameter
billing.saved? ? billing.update(#addr) : billing.save
raise "Billing is not saved for some reason: #{billing.errors.inspect}" unless billing && billing.saved?
user.billing_address = billing
user.save

Related

Retrieve records which have many-to-many association, using ruby and datamapper

I'm learning Sinatra, and I have read datamapper documentation and found this n to n relationship example:
class Photo
include DataMapper::Resource
property :id, Serial
has n, :taggings
has n, :tags, :through => :taggings
end
class Tag
include DataMapper::Resource
property :id, Serial
has n, :taggings
has n, :photos, :through => :taggings
end
class Tagging
include DataMapper::Resource
belongs_to :tag, :key => true
belongs_to :photo, :key => true
end
What I understood from the code above is that one photo may have many or zero tags, and a tag may have many or zero photos. How do I retrieve a list of photos with the tags associated to it already loaded. I know datamapper uses the lazy approach, so it does not automatically loads the associated classes (in this case photo.tag). So this:
photos = Photo.all
would result in an array with Photo objects without the tags. Is there a way to automatically retrieve it or do I have to iterate over the array and set that manually?
Thanks in advance!
I also have a database which has similar relations. Author, Post, Tag are main models, and Subscribedtag and Tagging are built through has n, :through.
class Author
include DataMapper::Resource
property :id, Serial
property :email, String, :unique => true
property :password, String
property :first_name, String
property :last_name, String
property :bio, Text
property :phone, String, :unique => true
property :twitter, String, :unique => true
property :facebook, String, :unique => true
property :show_phone, Boolean, :default => false
property :show_facebook, Boolean, :default => false
property :show_twitter, Boolean, :default => false
property :is_admin, Boolean, :default => false
property :this_login, DateTime
property :last_login, DateTime
property :session_lasting, Integer, :default => 0
has n, :posts
has n, :subscribedtags
has n, :tags, :through => :subscribedtags
end
class Post
include DataMapper::Resource
property :id, Serial
property :title, String, :required => true
property :body, Text, :required => true
property :is_blog_post, Boolean, :default => true
property :viewed, Integer, :default => 0
property :featured, Boolean, :default => false
property :created_at, DateTime
property :updated_at, DateTime
belongs_to :author
belongs_to :category
has n, :comments
has n, :taggings
has n, :tags, :through => :taggings
validates_length_of :title, :min => 3
validates_length_of :body, :min => 20
validates_format_of :title, :with => /\w/
#some other methods
end
class Tag
include DataMapper::Resource
property :id, Serial
property :name, String, :unique => true
has n, :taggings
has n, :posts, :through => :taggings
has n, :subscribedtags
has n, :authors, :through => :subscribedtags
validates_length_of :name, :min => 1
validates_format_of :name, :with => /\w/
# some other methods
end
class Tagging
include DataMapper::Resource
belongs_to :tag, :key => true
belongs_to :post, :key => true
end
class Subscribedtag
include DataMapper::Resource
belongs_to :tag, :key => true
belongs_to :author, :key => true
end
The way you've defined models, allows you to write queries, like that.
2.2.0 :016 > kant = Tag.get(25) # getting tag instance with id 25 and assign it to variable named kant
=> #<Tag #id=25 #name="İmmanuil Kant">
2.2.0 :017 > kant.posts
=> #returns post instances which has this tag.
2.2.0 :018 > kant.posts.count # count of posts with this tag.
=> 2
2.2.0 :021 > kant.posts.first.author.first_name
=> "Ziya" # check my author class and first_name attribute.
Let's say I want to retrieve the tag instances which has no posts.
a simple ruby command.
2.2.0 :024 > Tag.each {|tnp| puts tnp.name if tnp.posts.count == 0}
Latın
Python
Ruby
Sosializm
Hegel
Or retrieving tags based on posts.
2.2.0 :034 > p = Post.get(9)
=> #<Post #id=9 #title="Why we use Lorem Ipsum" #body=<not loaded> #is_blog_post=false #viewed=0 #featured=false #created_at=#<DateTime: 2015-08-02T23:14:04+05:00 ((2457237j,65644s,0n),+18000s,2299161j)> #updated_at=#<DateTime: 2015-08-02T23:14:04+05:00 ((2457237j,65644s,0n),+18000s,2299161j)> #author_id=1 #category_id=1>
2.2.0 :035 > p.tags
=> [#<Tag #id=19 #name="Bundesliqa">]
retrieve posts which has no tag.
2.2.0 :043 > Post.each {|pnt| puts pnt.id if pnt.tags.count.zero?}
8 #post with id has no tags
2.2.0 :044 > Post.get(8).tags.count
=> 0
you can also query via other attributes.
2.2.0 :046 > Tag.first(:name => "Lorem").id
=> 30
iterate over results
2.2.0 :050 > Tag.first(:name => "Lorem").posts.each {|lorempost| puts lorempost.title} # printing post titles which are tagged with lorem.
Get'em all
qwerty
I also associated authors with tags through Subscribedtags model, which I can easily check which author is subscribed to which tag, and vice versa.
2.2.0 :055 > z = Author.get(1)
=> # returns details of author instance
2.2.0 :056 > z.tags
=> [#, #, #, #]
or querying via Subscribedtag
2.2.0 :057 > z.subscribedtags
=> [#<Subscribedtag #tag_id=2 #author_id=1>, #<Subscribedtag #tag_id=4 #author_id=1>, #<Subscribedtag #tag_id=25 #author_id=1>, #<Subscribedtag #tag_id=30 #author_id=1>]
you can also define your own functions to utilize querying. I've defined a subscribed_tags method which returns an array of subscribed tags' names.
2.2.0 :058 > z.subscribed_tags
=> ["Həyat", "Məstan", "İmmanuil Kant", "Lorem"]
If I want to retrieve the first_name attribute of a random author, who is subscribed to tag named "Lorem",
2.2.0 :062 > Tag.first(:name => "Lorem").authors.sample.first_name
=> "Ziya"
As an answer to your 2nd question, yes, most times you have to iterate.
Because Photos.all return a collection of Photo object instances. And this instances individually has tag attributes, not the array consists of Photo instances.
if you call p = Photo.all; print p.tags; it will return all tags associated with all photos, which may or may not be the thing you want.
Feel free to ask more questions, if these are not enough.

Mongomapper embedded document "Cannot serialize an object" error

I'm quite new to mongodb and I'm using sinatra and mongomapper to update the values of an embedded document with the following set up:
class TeamMember
include MongoMapper::Document
key :name, String, :required => true
many :team_member_projects
end
class TeamMemberProject
include MongoMapper::EmbeddedDocument
key :date, Date, :required => true
one :project
end
class Project
include MongoMapper::Document
key :name, String, :required => true
end
The modifier code is:
team_member = TeamMember.find(params[:team_member])
project = Project.find(params[:project])
date = Date.parse(params[:date])
tm_project = TeamMemberProject.new(:project => project, :date => date)
team_member.push(:team_member_projects => tm_project)
team_member.save
but I get the error for .push line:
BSON::InvalidDocument at /project/add
Cannot serialize an object of class TeamMemberProject into BSON.
Did I not declare my embedded document properly? Or is there another way to update embedded documents, I don't know about. I'm trying to use: http://mongomapper.com/documentation/plugins/modifiers.html#push
This seems to work
team_member = TeamMember.find(params[:team_member])
project = Project.find(params[:project])
date = Date.parse(params[:date])
tm_project = TeamMemberProject.new(:project_id => project.id, :date => date)
team_member.team_member_projects << tm_project
team_member.save
It seems like I have to use project.id. Not sure why. Also not sure why my .push doesn't work, as I would have assumed it does the same thing as <<.

Rails -- updating a model in a database

So I ran into a little issue with validations -- I created a validation to ensure that no users in a database share identical email addresses. Then I created a user in the database. Afterward, I said user = User.find(1) which returned the user I had just created. Then I wanted to change its name so I said user.name = "New Name" and then tried to use user.save to save it back into the database. However, this command isn't working anymore (it returns false instead) and I think it has to do with my uniqueness validation test. Can someone help me with this problem?
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :email, #says that the name and email attributes are publicly accessible to outside users.
:password, :password_confirmation #it also says that all attributes other than name and email are NOT publicly accessible.
#this protects against "mass assignment"
email_regex = /^[A-Za-z0-9._+-]+#[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+[A-Za-z]$/ #tests for valid email addresses.
validates :name, :presence => true,
:length => {:maximum => 50}
validates :email, :presence => true,
:format => {:with => email_regex},
:uniqueness => {:case_sensitive => false}
validates :password, :presence => true,
:length => {:maximum => 20, :minimum => 6},
:confirmation => true
before_save :encrypt_password
def has_password?(submitted_password)
#compare encrypted_password with the encrypted version of the submitted password.
encrypted_password == encrypt(submitted_password)
end
def self.authenticate(email, submitted_password)
user = find_by_email(email)
if (user && user.has_password?(submitted_password))
return user
else
return nil
end
end
private
def encrypt_password
if (new_record?) #true of object has not yet been saved to the database
self.salt = make_salt
end
self.encrypted_password = encrypt(password)
end
def encrypt(string)
secure_hash("#{salt}--#{string}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string) #uses cryptological hash function SHA2 from the Digest library to encrypt the string.
end
def make_salt
secure_hash("#{Time.now.utc}--#{password}")
end
end
try save! and see what the exception tells you

Conflict the key name in MongoMapper

I use mongo mapper (0.8.6) in my sinatra service. I have one problem with stack level too deep. The problem is that there is conflict of the key "changes" in my model. Here is my model:
class ChangeLog
include MongoMapper::Document
belongs_to :resource
key :changes, Hash, :required => true
key :message, String, :required => true
key :note, String
key :user_uuid, String, :required => true
key :user_name, String, :required => true
timestamps!
end
However, I don't want to rename my key as in this case, it's the right name for my web service. Any suggestions?
changes is an instance method that will tell you what fields have changed since you last saved the document. Here's an example from MongoMapper's documentation
user = User.create(:name => 'John', :age => 29)
puts user.changed? # false
puts user.changes.inspect # {}
user.name = 'Steve'
puts user.changed? # true
puts user.changes.inspect # {"name"=>["John", "Steve"]}
Unfortunately, you're probably going to need to choose a different name for that field. Maybe "adjustments" or "variations" or "differences" or "modifications"?

Mongomapper query collection problem

When I define the User has_many meetings, it automatically creates a "user_id"
key/value pair to relate to the User collections. Except I can't run any
mongo_mapper finds using this value, without it returning nil or [].
Meeting.first(:user_id => "1234")
Meeting.all(:user_id => "1234")
Meeting.find(:user_id => "1234")
All return nil. Is there another syntax? Basically I can't run a query on the automatically generated associative ObjectId.
# Methods
class User
include MongoMapper::Document
key :user_name, String, :required => true
key :password, String
many :meetings
end
class Meeting
include MongoMapper::Document
key :name, String, :required => true
key :count, Integer, :default => 1
end
# Sinatra
get '/add' do
user = User.new
user.meetings "foobar") #should read: Meeting.new(:name => "foobar")
user.save
end
get '/find' do
test = Meeting.first(:user_id => "4b4f9d6d348f82370b000001") #this is the _id of the newly create user
p test # WTF! returns []
end
As Jimmy mentioned about checking Meeting.all, I don't think you would have anything.
Based on your example above I see a couple potential issues.
- Your User requires a :user_name so it's not getting saved
- would never get saved, because you didn't set the name which is required
- Your Meeting isn't getting saved either
- One more thing, you need to concat your meeting to user.meetings
This works with mongo_mapper 0.6.10
require 'rubygems'
require 'mongo_mapper'
MongoMapper.database = "meetings"
class User
include MongoMapper::Document
key :user_name, String, :required => true
key :password, String
many :meetings
end
class Meeting
include MongoMapper::Document
key :name, String, :required => true
key :count, Integer, :default => 1
end
user = User.create(:user_name => "Rubyist")
user.meetings << Meeting.create(:name => "foobar")
user.save
Meeting.first(:user_id => user.id)
User.find(user.id).meetings
You may have figured this out already, but I hope this is helpful anyway.
You can try using
Meeting.find_by_user_id "1234"
Also, if you run script/console then does Meeting.all show each record as having a user_id assigned to it?
What about just User.find("1234").meetings ?

Resources