I am wondering if there is any way to still use the .order("RANDOM()") with will_paginate so that when a page loads and it orders the pages, all the post will stay the same on each page until the home page is reloaded.
so to have all the posts on localhost:3000/posts?page=1 stay the same until localhost:3000(root_path) is visited again.
Problem is it will paginate posts but it current re orders them for each page selected so you will often see posts on page 1 also on page 2.
One way to do this is to set the random seed which your database is ordering by, such that it returns the same sequence of random numbers each time. You can store this seed in your users' session, and reset it only when you want to. However, there's a complication -- even though setting the random seed produces the same ordering of random numbers each time, there's no guarantee your database will execute it on your rows in the same order each time, unless you force it to do so like so:
SELECT items.*
FROM (SELECT setseed(0.2)) t
, (SELECT name, rank() OVER (ORDER BY name DESC)
FROM foos ORDER BY name DESC) items
JOIN generate_series(1, (SELECT COUNT(*) FROM foos))
ON items.rank = generate_series
ORDER BY RANDOM()
LIMIT 10;
As you can tell, that's quite complicated, and it forces your database to materialize your entire table into memory. It'd work for smaller data sets, but if you've got a big data set, it's out of the question!
Instead, I'd suggest you go with a solution more like tadman suggested above: generate a page of results, store the ids into session, and when you need to generate the next page, simply ignore anything you've already shown the user. The code would look like:
class ThingsController < ApplicationController
def index
#page = params[:page].to_i
session[:pages] ||= {}
if ids = session[:pages][#page]
# Grab the items we already showed, and ensure they show up in the same order.
#things = Things.where(id: ids).sort_by { |thing| ids.index(thing.id) }
else
# Generate a new page of things, filtering anything we've already shown.
#things = Things.where(["id NOT IN (?)", shown_thing_ids])
.order("RANDOM()")
.limit(30) # your page size
# Save the IDs into our session so the above case will work.
session[:pages][#page] = #things.map(&:id)
end
end
private
def shown_thing_ids
session[:pages].values.flatten
end
end
This method uses the session to store which IDs were shown on each page, so you can guarantee the same set of items and ordering will be shown if the user goes back. For a new page, it will exclude any items already displayed. You can reset the cache whenever you want with:
session.delete(:pages)
Hope that helps! You could also use Redis or Memcache to store your page data, but the session is a good choice if you want the ordering to be random per-user.
Related
I have a Rails application. It has a feed that shows items from different users all mixed up. It would be something similar to Pinterest in the way you see these items.
Right now I show all these items ordered by its date of creation. However, as the items are created by batches by users, they are shown not randomly (say you se the first 6 items being from one user, then the other 5 from other one, etc.).
The code that serves the items is this:
class Feeder
def self.most_recent_created(watching_user=nil, current_cursor)
next_cursor = nil
feed = []
influencers_ids = User.influencers.distinct(:_id)
Rating.most_recent_from_influencers(watching_user, influencers_ids).scroll(current_cursor) do |rating, cursor|
next_cursor = cursor
feed << ImoPresenter.new(Imo.new(rating), watching_user)
end
feed << next_cursor.to_s
end
end
scroll just gives a cursor pointing to each item in the iteration. Then I push the item into the feed.
The access to the database is done in Rating.most_recent_from_influencers(watching_user, influencers_ids), where most_recent_from_influencers(watching_user, influencers_ids) is a scope defined as follows:
scope :not_from, ->(user) { ne(user_id: user.id) }
scope :from, ->(user_ids) { any_in(user_id: user_ids) }
scope :most_recent_from_influencers, ->(watching_user, influencers_ids) {
proxy = from(influencers_ids).over_zero.desc(:created_at).limit(IMOS_PER_PAGE)
proxy = proxy.not_from(watching_user) if watching_user
proxy
}
MongoDB does not have random access out of the box. They suggest this for having a way of accessing randomly to the items. Basically, the solution is to add a random field in all documents and order the collection through this field. However, although I would have random items, I would always have almost the same items being shown, as I would just have the options of ordering it by desc(:rand) or asc(:rand).
I would like to have suggestions on how I can make the items being shown truly in a random way. Is it possible?
Based on similar questions, I've come to the conclusion that having a random field is a valid solution if the collection is dynamic, meaning that documents are inserted frequently. The more dynamic the collection is, the more 'random' access you can have.
Disclaimer: I'm new to Rails.
What I'm trying to do is this in the long form:
user = Spree::User.find(2)
cart = Spree::Order.where(state: "cart", user_id: user.id)
line_item = Spree::LineItem.where(order_id: cart.last.id).map { |order| order.variant_id}
variant = Spree::Variant.find(line_item).map { |order| order.product_id }
Spree::Product.find(variant).map { |product| product.name }
What happens is it goes into the DB, finds the user, gets an order where their state is in cart. Then goes and finds its line item. Goes over to find the "variant" data to find out what products are included. Then over to the products page to tell me all products in a users cart.
This looks ugly and garjumbled. You wouldn't happen to know a way to refactor it and not do so much querying?
If you look at Spree Order States, it's not that you have multiple orders in different states, just one order that could be in one particular state.
It really depends on Spree::User. I can't find documentation and/or source for Spree::User, but mentions to a certain LegacyUser.
Since in the comments you mentioned you have a custom build, you need to check the actual code for Spree::User and see if a user can have one or many orders, and if there's already a method for selecting cart state orders.
So if Spree::User has one order:
order = user.order
order.variants.map(&:name)
Else:
order = user.orders.where(state: "cart")
order.variants.map(&:name)
There could be a method to get the order you need from the user in the code.
I'm writing an application that allows users to send one another messages about an 'offer'.
I thought I'd save myself some work and use the Mailboxer gem.
I'm following a test driven development approach with RSpec. I'm writing a test that should ensure that only one Conversation is allowed per offer. An offer belongs_to two different users (the user that made the offer, and the user that received the offer).
Here is my failing test:
describe "after a message is sent to the same user twice" do
before do
2.times { sending_user.message_user_regarding_offer! offer, receiving_user, random_string }
end
specify { sending_user.mailbox.conversations.count.should == 1 }
end
So before the test runs a user sending_user sends a message to the receiving_user twice. The message_user_regarding_offer! looks like this:
def message_user_regarding_offer! offer, receiver, body
conversation = offer.conversation
if conversation.nil?
self.send_message(receiver, body, offer.conversation_subject)
else
self.reply_to_conversation(conversation, body)
# I put a binding.pry here to examine in console
end
offer.create_activity key: PublicActivityKeys.message_received, owner: self, recipient: receiver
end
On the first iteration in the test (when the first message is sent) the conversation variable is nil therefore a message is sent and a conversation is created between the two users.
On the second iteration the conversation created in the first iteration is returned and the user replies to that conversation, but a new conversation isn't created.
This all works, but the test fails and I cannot understand why!
When I place a pry binding in the code in the location specified above I can examine what is going on... now riddle me this:
self.mailbox.conversations[0] returns a Conversation instance
self.mailbox.conversations[1] returns nil
self.mailbox.conversations clearly shows a collection containing ONE object.
self.mailbox.conversations.count returns 2?!
What is going on there? the count method is incorrect and my test is failing...
What am I missing? Or is this a bug?!
EDIT
offer.conversation looks like this:
def conversation
Conversation.where({subject: conversation_subject}).last
end
and offer.conversation_subject:
def conversation_subject
"offer-#{self.id}"
end
EDIT 2 - Showing the first and second iteration in pry
Also...
Conversation.all.count returns 1!
and:
Conversation.all == self.mailbox.conversations returns true
and
Conversation.all.count == self.mailbox.conversations.count returns false
How can that be if the arrays are equal? I don't know what's going on here, blown hours on this now. Think it's a bug?!
EDIT 3
From the source of the Mailboxer gem...
def conversations(options = {})
conv = Conversation.participant(#messageable)
if options[:mailbox_type].present?
case options[:mailbox_type]
when 'inbox'
conv = Conversation.inbox(#messageable)
when 'sentbox'
conv = Conversation.sentbox(#messageable)
when 'trash'
conv = Conversation.trash(#messageable)
when 'not_trash'
conv = Conversation.not_trash(#messageable)
end
end
if (options.has_key?(:read) && options[:read]==false) || (options.has_key?(:unread) && options[:unread]==true)
conv = conv.unread(#messageable)
end
conv
end
The reply_to_convesation code is available here -> http://rubydoc.info/gems/mailboxer/frames.
Just can't see what I'm doing wrong! Might rework my tests to get around this. Or ditch the gem and write my own.
see this Rails 3: Difference between Relation.count and Relation.all.count
In short Rails ignores the select columns (if more than one) when you apply count to the query. This is because
SQL's COUNT allows only one or less columns as parameters.
From Mailbox code
scope :participant, lambda {|participant|
select('DISTINCT conversations.*').
where('notifications.type'=> Message.name).
order("conversations.updated_at DESC").
joins(:receipts).merge(Receipt.recipient(participant))
}
self.mailbox.conversations.count ignores the select('DISTINCT conversations.*') and counts the join table with receipts, essentially counting number of receipts with duplicate conversations in it.
On the other hand, self.mailbox.conversations.all.count first gets the records applying the select, which gets unique conversations and then counts it.
self.mailbox.conversations.all == self.mailbox.conversations since both of them query the db with the select.
To solve your problem you can use sending_user.mailbox.conversations.all.count or sending_user.mailbox.conversations.group('conversations.id').length
I have tended to use the size method in my code. As per the ActiveRecord code, size will use a cached count if available and also returns the correct number when models have been created through relations and have not yet been saved.
# File activerecord/lib/active_record/relation.rb, line 228
def size
loaded? ? #records.length : count
end
There is a blog on this here.
In Ruby, #length and #size are synonyms and both do the same thing: they tell you how many elements are in an array or hash. Technically #length is the method and #size is an alias to it.
In ActiveRecord, there are several ways to find out how many records are in an association, and there are some subtle differences in how they work.
post.comments.count - Determine the number of elements with an SQL COUNT query. You can also specify conditions to count only a subset of the associated elements (e.g. :conditions => {:author_name => "josh"}). If you set up a counter cache on the association, #count will return that cached value instead of executing a new query.
post.comments.length - This always loads the contents of the association into memory, then returns the number of elements loaded. Note that this won't force an update if the association had been previously loaded and then new comments were created through another way (e.g. Comment.create(...) instead of post.comments.create(...)).
post.comments.size - This works as a combination of the two previous options. If the collection has already been loaded, it will return its length just like calling #length. If it hasn't been loaded yet, it's like calling #count.
It is also worth mentioning to be careful if you are not creating models through associations, as the related model will not necessarily have those instances in its association proxy/collection.
# do this
mailbox.conversations.build(attrs)
# or this
mailbox.conversations << Conversation.new(attrs)
# or this
mailbox.conversations.create(attrs)
# or this
mailbox.conversations.create!(attrs)
# NOT this
Conversation.new(mailbox_id: some_id, ....)
I don't know if this explains what's going on, but the ActiveRecord count method queries the database for the number of records stored. The length of the Relation could be different, as discussed in http://archive.railsforum.com/viewtopic.php?id=6255, although in that example, the number of records in the database was less than the number of items in the Rails data structure.
Try
self.mailbox.conversations.reload; self.mailbox.conversations.count
or perhaps
self.mailbox.reload; self.mailbox.conversations.count
or, if neither of those work, just try reloading as many of the objects as possible to see if you can get it to work (self, mailbox, conversations, etc.).
My guess is that something is messed up between memory and the DB. This is definitely a really weird error though, might wanna put in an issue on Rails to see why this would be the case.
The result of mailbox.conversations is cached after the first call. To reload it write mailbox.conversations(true)
I put many question about caching in here but no one answer. So mayby i will have answer on this.
I have in my search model code like this:
users = users.where('id >?', my_value)
Rails.cache.fetch('my_cached',expires_in: 3.minutes) do
users = users.order('current_sign_in_at DESC')
end
Can somebody explain me what this do. I thought that this will return sorted users table and put it in cache for 3 minutes so when i require next search it will return me my_cached result if it doesnt expired.
But it does not work like that. When some user login and current_sign_in_at is changed - cache is override and new query is returned.
I am not very experienced with the caching procedure for RoR, but I think what you say is right.
I thought that this will return sorted users table and put it in cache
for 3 minutes so when i require next search it will return me
my_cached result if it doesnt expired.
Then, I think that the caching should be done in this matter:
Rails.cache.fetch('my_cached',expires_in: 3.minutes) do
User.where('id >?', my_value).order('current_sign_in_at DESC')
end
Remember, that can exist some rules that can expire the cache before the expiration time assigned. This depends how the User model is configured.
Again, the query will not be executed only if you use the variable my_cached. Maybe the query you see is coming from another procedure (maybe from Devise itself if you use it?)
SOME HELPFUL REFERENCE
https://devcenter.heroku.com/articles/caching-strategies#low-level-caching
http://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-store
http://robotmay.com/post/23161612605/everyone-should-be-using-low-level-caching
UPDATE
If the variable my_value changes frequently (can be an aleatory value), then the cache will be used only if the my_value is the same as a previous (in 3 minutes) value.
Maybe an alternative solution for a variable my_value can be:
Rails.cache.fetch('my_cached',expires_in: 3.minutes) do
User.all # cache all users
end
# then filter the cached value by the my_value and order it
I'm trying to count current viewers on the particular page. I need this count to be stored in the DB. The main trouble is to clean up after user leaves the page.
Users are anonymous. Every active user sends AJAX-request every 5 seconds.
What's the best algorithm to do that? Any suggestions?
UPD: I'm trying to reduce amount of queries to the DB, so, I think, I don't really need to store that count in the DB while I can access it other way from the code.
Don't even think about storing this in database, your app will be incredibly slowed down.
So use Cache for this kind of operation.
To count the number of people, I'd say:
assign a random ID to each anonymous user and store it in his session
send the ID within your ajax call
store an Array of Hashes in cache with [{ :user_id, :latest_ping }, {} ] (create a cache var for each page)
delete the elements of the array which appear to be too old
you've your solution: number of users = nb of elements in the array
If you store the users in the database somehow, you could store a last_seen_at field in the users table, and update that with Time.now for every AJAX request that user sends.
To display how many users you currently have, you can just perform a query such as:
#user_count = User.where("last_seen_at < ?", 5.seconds.ago).count
If you want to clean up old users, I suggest that you run some kind of cron job, or use the whenever gem, or something like that, to periodically delete all users that haven't been seen for some time.
I would suggest you create a model that contains a unique key (cookie-id or something) that you save or update with every AJAX heartbeat request.
You then have a session controller that could look like this:
def create
ActiveUser.where(:cookie => params[:id]) || ActiveUser.new
ActiveUser.cookie = prams[:id]
ActiveUser.timestamp = Time.now
ActiveUser.save
end
Your number of active users is then simply a SELECT COUNT(*) FROM ActiveUsers WHERE timestamp > NOW() - 5 or something like that.
Martin Frost is on the right track. There's the #touch method to update last_seen_at: user.touch(:last_seen_at)
But it would be even more efficient to just update the user without having to fetch the model from the database:
> User.update_all({:last_seen_at => Time.now}, {:id => params[:user_id})
SQL (3.1ms) UPDATE "users" SET "last_seen_at" = '2011-11-17 12:37:46.863660' WHERE "users"."id" = 27
=> 1