rails scope for map table - mysql

I have the following models:
class Message < ActiveRecord::Base
has_many :read_messages
module Scopes
def by_read_status(read_status, user_id)
case read_status
when 'Unread'
when 'Read'
joins(:read_messages).where(:read_messages => {:read => true, :admin_user_id => user_id})
else
where('1 = 1') # no scope necessary
end
end
end
extend Scopes
end
and...
class ReadMessage < ActiveRecord::Base
has_many :admin_users
has_many :messages
end
I have a controller method called 'mark_as_read' for the message. It just creates a new read_message record with that message id, and the admin user id of the person that marked as read. Since messages are global, I want the ability for each user of the system to manage the read status seperately (which is why I have that extra layer in there). So as you can see by my scope, the by_read_status('Read', user_id) would return all records where it finds a mapped record with read true. The problem is, how can I do the opposite? (return all records where it does not find a map record, OR the map record :read is set to false)?
I am using the scopes like this:
#search = Message.search(params[:q])
messages = #search.result.accessible_by(current_ability)
messages = messages.by_company(session[:company_filter]) if session[:company_filter] && session[:company_filter] > 0
messages = messages.by_campaign(session[:campaign_filter]) if session[:campaign_filter] && session[:campaign_filter] > 0
read_filter = params[:read].blank? ? 'Unread' : params[:read]
messages = messages.by_read_status(read_filter, current_admin_user.id)
messages = messages.page(params[:page]).per(20)
#messages = MessageDecorator.new(messages)
So you can see in the middle there of my scopes, i've got the by_read_status. If I return an array, or something other than a scoped object, it will throw a fit. Can anyone help me figure out how to do the 'Unread' portion of my scope?
Thanks!
Edited Answer
exclude = Message.by_read_status('Read', user_id).map(&:id)
exclude = [0] unless exclude.size > 0
where("id not in (?)", exclude)

where("id not in (?)", by_read_status('Read', user_id))

Related

can't create a record in a database

I am using rails version 4.2 and ruby version 2.2.0. I am trying to save a record to lollypops table. No exceptions indicating reasons.
TASK: As soon as a member is created and saved, I want to populate the lollypops table by calling the create_lollypop(#member.id) in members controller's create method like this:
# POST /members
# POST /members.json
def create
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
create_lollypop(#member.id) #From here I want to create lollypop
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
end
def create_lollypop(member_id)
#member = Member.find(member_id)
Lollypop.create(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name)
end
The 'member' is created but the 'lollypops' table is not populated. The associations are:
MEMBER model:
has_one :lollypop, :dependent=>:destroy
LOLLYPOP model
belongs_to :member
If I use generic SQL command then the lollypops table gets populated but I do not want to do that:
def self.create_lollypop(member_id)
member = Member.find(member_id)
ActiveRecord::Base.connection.execute("insert into lollypops (member_id,product_name,product_price,email,house_flat,street,city_town,country,postcode_index,name)
values(#{member.id},'lollypop',#{100},'#{member.email}','#{member.house_flat}','#{member.street}','#{member.city_town}','#{member.country_code}','#{member.postcode_index}','#{member.name}')")
end
Any advice would be welcomed. Thank you.
In your create_lollypop(), You are not defining #member.
def create_lollypop(member_id)
#member = Member.find member_id
Lollypop.create!(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
end
Also use create! so in case any validation failed then it will raise exception. So it will help you sort out issue.
For the moment try to create lollypop using the association method create_lollypop directly in your controller. use this code in you create controller method, note that create_lollypop method will fill (member_id field automatically):
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
#From here I want to create lollypop
#member.create_lollypop(
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This is not exactly an answer, more like tips and notes, it's a little long and I hope you don't mind.
return unless request.post?
This is more of a php thing not a rails thing, in rails already the routing is checking this, so you don't need to do this check inside the controller, if it isn't a post it will be routed elsewhere.
#member = Member.create(members_params)
return unless request.post?
#member.save!
Saving after creating is meaningless, because create already saves the data, if you are doing it for the sake of the bang save!, then you could use the create with bang create!, not to mention that you do the redirection check after the member's create, so if this did work, it would leave you with stray members.
c = Country.find(#member.country_id)
#member.update_attributes(:country_code=>c.code)
If you have your assocciations correctly, you don't need to save the code like this, because the member knows that this country_id belongs to a country.
So add this to the member model
class Member < ActiveRecord::Base
has_one :lollypop, dependent: :destroy
belongs_to :country
end
This way you could always call #member.country to return the country object, then the code could come from there, like #member.country.code, or you could just write a method to shorten that up
def country_code
country.code
end
this way will get the code through an extra query, but it has an advantage, if you for any reason change a country's code, you don't need to loop on all members who have that country and update their codes too, you could also shorten this up even more using #delegate
#member.save!
#member.update_attributes(:country_code=>c.code)
Here you are updating the attributes of member after saving the member, which is kinda a waste, because you are doing 2 queries for what could be done with 1 query, programmatically it is correct and it will work, but it's bad for scaling, when more users start using your app the database will be more busy and the responses will be slower.
Instead i would recommend to postpone the creation of member till you have all the data you want
#member = Member.new(members_params) # this won't save to the database yet
#memeber.code = Country.find(#member.country_id).code
#member.save
This will only do 1 query at the end when all data is ready to be saved.
redirect_to(:controller => '/admin/members', :action => 'show', :id=> #member.id)
This is ok, but you probably have a better shorter path name in your routes, something like members_admin_path, check your routes name by doing a bin/rake routes in your terminal.
redirect_to members_admin_path(id: #member)
redirect_to ...
flash[:notice] = "message"
I'm not sure this will work, because the redirection needs to be returned, but when you added the flash after it, either the redirection will happen without the flash, or the flash will be set and returned as it's the last statement, but the redirection won't happen, not sure which will happen, to fix it you can simply swap the two statements, create the flash first and then redirect, or use the more convenient way of setting the flash while redirecting, cause that's supported
redirect_to ....., notice: 'my message'
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This will do the job, but it isn't conventional, people tend to use the soft save and then do an if condition on the return value, either true or false, here's a short layout
# prepare #member's data
if #member.save
# set flash and redirect
else
load_data
render :new
end
The lollypop creation
Now there's a few things about this, first you have the method in the controller, which is bad cause it shouldn't be the controller's concern, the second method the self.create_lollypop is better cause it's created on the model level, but it's a class method, then the better way is creating it as a member method, this way the member who creates the lollypop already knows the data because it's his own self, notice i don't need to call #member because i am already inside member, so simple calls like id, email will return the member's data
# inside member.rb
def create_lollypop
Lollypop.create!(
member_id: id,
product_name: 'lollypop',
product_price: 100,
email: email,
house_flat: house_flat,
street: street,
city_town: city_town,
country: country,
postcode_index: postcode_index,
name: name
)
end
if you want you can also add this as an after create callback
after_create :create_lollypop
ps: This method name will probably conflict with the ActiveRecords create_lollypop method, so maybe you should pick a different name for this method.
As Mohammad had suggested to me, I changed Lollypop.create to Lollypop.create! and
while running my code, one validation error popped up. After correcting it and
altering my code to:
Lollypop.create!(
:member_id=> #member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country_code,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
The 'lollypops' table got populated.

Ruby ActiveRecord Query with has_many Association

I have a system that has a User, Message, and MessageToken models. A User can create Messages. But when any User reads the Messages of others a MessageToken is created that associates the reader (User) to the Message. MessageTokens are receipts that keep track of the states for the user and that particular message. All of my associations in the Models are set up properly, and everything works fine, except for structuring a very specific query that I cannot get to work properly.
User.rb
has_many :messages
Message.rb
belongs_to :user
has_many :message_tokens
MessageToken.rb
belongs_to :user
belongs_to :message
I am trying to structure a query to return Messages that: Do not belong to the user; AND { The user has a token with the read value set to false OR The user does not have a token at all }
The later part of the statement is what is causing problems. I am able to successfully get results for Messages that are not the user, Messages that the user has a token for with read => false. But I cannot get the expected result when I try to make a query for Messages that have no MessageToken for the user. This query does not error out, it just does not return the expected result. How would you structure such a query?
Below are the results of my successful queries and the expected results.
130 --> # Messages
Message.count
78 --> # Messages that are not mine
Message.where.not(:user_id => #user.id)
19 --> # Messages that are not mine and that I do not have a token for
59 --> # Messages that are not mine, and I have a token for
Message.where.not(:user_id => #user.id).includes(:message_tokens).where(message_tokens: {:user_id => #user.id}).count
Message.where.not(:user_id => #user.id).includes(:message_tokens).where(["message_tokens.user_id = ?", #user.id]).count
33 --> # Messages that are not mine, and I have a token for, and the token is not read
Message.where.not(:user_id => #user.id).includes(:message_tokens).where(message_tokens: {:user_id => #user.id, :read => false}).count
Message.where.not(:user_id => #user.id).includes(:message_tokens).where(["message_tokens.user_id = ? AND message_tokens.read = false", #user.id]).references(:message_tokens).count
The Final Expected Result
52 --> # Messages that are not mine and: I have a token for that is not read OR I do not have a token for
My best attempt at a query to achieve my goal
64 --> # Wrong number returned, expected 52
Message.where.not(:user_id => #user.id).includes(:message_tokens).where(["(message_tokens.user_id = ? AND message_tokens.read = false) OR message_tokens.user_id <> ?", #user.id, #user.id]).references(:message_tokens).count
The problem lies in the query trying to find Messages that are not the users and that the user does not have a token for
63 --> #This does not yield the expected result, it should == 19 (the number of Messages that are not mine and that I do not have a token for)
Message.where.not(:user_id => #user.id).includes(:message_tokens).where.not(message_tokens: {:user_id => #user.id}).count
Message.where.not(:user_id => #user.id).includes(:message_tokens).where(["message_tokens.user_id <> ?", #user.id]).references(:message_tokens).count
How can I solve this?
If you don't mind using 2 queries, a possible solution would be:
messages_not_written_by_user = Message.where.not(:user_id => #user.id)
messages_already_read_by_user = Message.where.not(:user_id => #user.id).includes(:message_tokens).where(message_tokens: {:user_id => #user.id, :read => true})
messages_not_read_by_user_yet = messages_not_written_by_user - messages_already_read_by_user
I would personally find this syntax more readable:
messages_not_written_by_user = Message.where.not(:user => #user).count
messages_already_read_by_user = Message.where.not(:user => #user).includes(:message_tokens).where(message_tokens: {:user => #user, :read => true}).count
One remark to this query:
63 --> #This does not yield the expected result, it should == 19 (the number of Messages that are not mine and that I do not have a token for)
Message.where.not(:user_id => #user.id).includes(:message_tokens).where.not(message_tokens: {:user_id => #user.id}).count
This query searches for all the messages which have a token with an arbitrary other user. (If msg1 has a token with #user, and it also has a token with #another_user, this query will find it.)
Full disclosure - I'm not sure how I'd do this as you have it set up right now. However: are you against installing a gem to help? If you're not, I'd suggest you look into the Squeel gem (https://github.com/activerecord-hackery/squeel).
Squeel makes these kinds of associations a lot easier and allows use to use the plain old | operator. It's built on Arel and shouldn't effect anything you've written in ActiveRecord (at least in my experience). Hope that helps!
Ok, so thanks to the help of R11 Runner I was able to come up with a solution, which required using pure SQL. I could not use the Squeel gem or ActiveRecord as there was no equivalent to SQL's NOT EXISTS operator, which was the crucial component missing.
The reason this works is because unlike the other solutions the NOT EXISTS operator will return all records from the Messages table where there are no records in the MessageTokens table for the given user_id, whereas using where.not would look for the first match instead not ensuring the non existence that was needed.
Message.find_by_sql ["SELECT * FROM messages where messages.user_id <> ?
AND (
(EXISTS (SELECT * FROM message_tokens WHERE message_id = messages.id AND user_id = ? AND read = FALSE))
OR
(NOT EXISTS (SELECT * FROM message_tokens WHERE message_id = messages.id AND user_id = ?))
)",#user.id, #user.id, #user.id]

Query with OR condition through an association

I'm trying to facilitate finding a user record via either username or email. THe only issue is that the username is stored on an associated UserDetail model.
class User
has_one :user_detail
def self.find_by_username_or_email(value)
# This is effectively pseudocode for the query I'd like to write.
query = "lower(email) = :value OR user_detail.username = :value"
where([query, { value: value }]).first
end
end
How can I write a query which matches either the email or the username on the associated user_detail record?
You were almost there!
This should work for you:
class User < ActiveRecord::Base
has_one :user_detail
def self.find_by_username_or_email(value)
query = "lower(users.email) = :value OR user_details.username = :value"
includes(:user_detail).where(query, value: value).first
end
end
seems like you need to write
self.includes(:user_detail).where(" user.email = ? OR user_detail.user_name =?", param[1], param[2])

How do I iterate over an array with Ruby so that it saves individual records in MySQL?

In my HTML, I am able to return an array from a select multiple box using
<select multiple id="purchases" name="purchases[]">
<option value="purchase1">Shoes</option>
<option value="purchase2">Dress Shirts</option>
</select>
My goal is to create a new database record for each of the options selected (I'm using Ruby on Rails and MySQL.) However, my controller isn't saving each value in its own record:
Controller
#purchase = Purchase.new(params[:purchases])
#purchase.purchaseinfo = params[:purchases]
Purchase Model
class Purchase < ActiveRecord::Base
belongs_to :customer
end
Customer Model
class Customer < ActiveRecord::Base
belongs_to :account
has_many :purchases
end
I know I should iterate through the controller, but I'm not sure how. Thanks in advance for your advice!
Edit 1
No matter what I do in the controller, the log tells me that the whole array, "purchases", is being sent, not the individual records. Here is the log and here is the current controller.
LOG
Processing SavedcustomerController#create (for IP at DATE TIME) [POST]
Parameters: {"purchases"=>["Item1", "Item2", "Item3"]}
Redirected to http://example.com/maptry
Completed in 21ms (DB: 2) | 302 Found [http://example.com/]
SavedcustomerController#Create
items_array = params[:purchases].split('"\"\"",')
items_array.each do |arrayitem|
#purchase = Purchase.new(params[:purchases])
#purchase.purchaseinfo = arrayitem
end
If you are on Rails 3 and you add attr_accessible :purchase_info to your Purchase model you could do this.
purchases = params[:purchases]
#purchases = purchases.map { |purchase| Purchase.create(purchase_info: purchase) }
UPDATE
In the most simple way you should be able to just do
purchases = params[:purchases]
purchases.each do |purchase_info|
purchase = Purchase.new
purchase.purchase_info = purchase_info
purchase.save
end
I'm not sure if attr_accessible was in Rails 2 but that code up there should work... are you getting any exceptions/errors with the code I provided?
Can you try this:
items_array = params[:purchases]
items_array.each do |arrayitem|
#purchase = Purchase.new()
#purchase.purchaseinfo = arrayitem
end
In Purchase.new() you should put all other attributes you want

Ruby libxml parsing and inserting to database

I am currently trying to read from an xml file which records the jobs on a PBS. I have succesfullly managed to parse the code, but am unable to insert the objtects into my database, i receive this error:
"You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.delete"
This is my Model:
require 'xml/libxml'
class Job < ActiveRecord::Base
JOB_DIR = File.join('data', 'jobs')
attr_reader :jobid, :user, :group, :jobname, :queue, :ctime
attr_reader :qtime, :etime, :start, :owner
def initialize(jobid, user, group, jobname, queue, ctime, qtime, etime, start, owner)
#jobid, #user, #group, #jobname, #queue = jobid, user, group, jobname, queue
#ctime, #qtime, #etime, #start, #owner = ctime, qtime, etime, start, owner
end
def self.find_all()
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
node.find('jobid').to_a.first.content,
node.find('user').to_a.first.content,
node.find('group').to_a.first.content,
node.find('jobname').to_a.first.content,
node.find('queue').to_a.first.content,
node.find('ctime').to_a.first.content,
node.find('qtime').to_a.first.content,
node.find('etime').to_a.first.content,
node.find('start').to_a.first.content,
node.find('owner').to_a.first.content
)
end
jobs
end
end
An my Model Controller:
class JobController < ApplicationController
def index
#jobs = Job.find_all()
end
def create
#jobs = Job.find_all()
for job in #jobs
job.save
end
end
end
I would appreciate any help...Thank you!
I'm not sure on the causes of the error message you're seeing because I can't see anywhere that you're trying to invoke a delete method, however this does seem like a slightly confused use of ActiveRecord.
If you have a jobs database table with fields jobid, user, group, jobname etc. then ActiveRecord will create accessor methods for these and you should not be using attr_reader or overriding initialize. You should also not be setting values value instance variables (#jobid etc.) If you don't have such fields on your table then there is nothing in your current code to map the values from the XML the database fields.
Your def self.find_all method should probably be along the lines of:
def self.build_from_xml
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
:jobid => node.find('jobid').to_a.first.content,
:user => node.find('user').to_a.first.content,
...
Rails used to have a method of its own find_all to retrieve all existing records from the database so your method name is probably a bit misleading. Rails tends to use the build verb to mean create a new model object but don't save it yet so that's why I've gone with a name like build_from_xml.