Say an application has many Products and searching capabilities.
1. How can I track page views on each product(a simple counter) and
2. how can I track search queries as well?
Most importantly, I need to be able to find/order products by number of page views.
The (simpler / best performance / least outside dependency) the better!
Why this question? The solutions I've seen so far are either out of date gems or don't work well with searching.
Here's a simple way to do it. In the model:
def increment(by = 1)
self.views ||= 0 #Prevents error if views can be nil
self.views += by
self.save
end
Then in the controller:
#model.increment
Faster if made into an asynchronous job which I still need to do.
To prevent users spamming refresh or something I made it a bit more complicated:
def show
product_itemcode = params[:itemcode]
#product = Product.find_by(itemcode: product_itemcode)
unless session[product_itemcode.to_s]
#product.increment
session[product_itemcode.to_s] = true
end
end
Any thoughts?
EDIT:
An increment method already exists:
http://apidock.com/rails/ActiveRecord/Persistence/increment
You still have to call object.save though
Related
TLDR
Is there a way to mark cached values so I could do something like:
cache.filter('some_tag').clear()
Details
In my project I have the following model:
class Item(models.Model):
month = models.DateField('month', null=False, blank=False, db_index=True)
kg = models.BigIntegerField('kg')
tags = models.ManyToManyField('Tag', related_name='items')
// bunch of other fields used to filter data
And I have a report_view that returns the sum of kg by month and by tag according to the filters supplied in the URL query.
Something like this:
--------------------------------
|Tag |jan |fev |mar |
--------------------------------
|Tag 1 |1000 |1500 |2000 |
--------------------------------
|Tag 2 |1235 |4652 |0 |
--------------------------------
As my Item table has already more than 4 million records and is always growing my report_view is cached.
So far I got all of this covered.
The problem is: the site user can change the tags from the Items and every time this occurs I have to invalidate the cache, but I would like to do it in a more granular way.
For example if a user changes a tag in a Item from january that should invalidate all the totals for that month (I prefer to cache by month because sometimes changing one tag has a cascading effect on others). However I don't know all the views that have been cached as there are thousands of possibilities of different filters that change the URL.
What I have done so far:
Set a signal to invalidate all my caches when a tag changes
#receiver(m2m_changed, sender=Item.tags.through)
def tags_changed(sender, **kwargs):
cache.clear()
But this cleans everything which is not optimal in my case. Is there a way of doing something like cache.filter('some_tag').clear() with Django cache framework?
https://martinfowler.com/bliki/TwoHardThings.html
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
Presuming you are using Django's Cache Middleware, you'll need to target the cache keys that are relevant. You can see how they generate the cache key from these two files in the Django Project:
-
https://github.com/django/django/blob/master/django/middleware/cache.py#L99
- https://github.com/django/django/blob/master/django/utils/cache.py#L367
- https://github.com/django/django/blob/master/django/utils/cache.py#L324
_generate_cache_key
def _generate_cache_key(request, method, headerlist, key_prefix):
"""Return a cache key from the headers given in the header list."""
ctx = hashlib.md5()
for header in headerlist:
value = request.META.get(header)
if value is not None:
ctx.update(force_bytes(value))
url = hashlib.md5(force_bytes(iri_to_uri(request.build_absolute_uri())))
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (key_prefix, method, url.hexdigest(), ctx.hexdigest())
return _i18n_cache_key_suffix(request, cache_key)
The cache key is generated based on attributes and headers from the request and hashed values (i.e. the url is hashed and used as part of the key). The Vary header in your response specifies other headers to use as part of the cache it.
If you understand how Django is caching your views and calculating your cache keys, then you can use this to target appropriate cache entries, but this is still very difficult because the url is hashed you can't target url patterns (you could use https://stackoverflow.com/a/35629796/784648 cache.delete_patterns(...) otherwise).
Django primarily relies on timeout to invalidate the cache.
I would recommend looking into Django Cacheops, this package is designed to work with Django's ORM to cache and invalidate QuerySets. This seems a lot more practical for your needs because you want fine-grained invalidation on your Item QuerySets, you simply will not get that from Django's Cache Middleware. Take a look at the github repo, I've used it and it works well if you take the time to read the docs and understand it.
I want to get a view count for every time a photo is viewed. This is my show method, not sure why this doesn't work, seems pretty straight forward. The branch is called every time, but the database is not bing update...
def show
if current_user.nil? || #photo.profile != current_user.profile
#photo.views += 1
#photo.save!
end
render layout: 'layouts/photo'
end
The problem here is that #profile is simply not specified.
You may want to do something like the following:
def show
#profile = Profile.find(params[:id])
if #profile && current_user.nil? || #photo.profile != current_user.profile
#profile.views += 1
#profile.save
end
render layout: 'layouts/photo'
end
Besides that it's probably best to use a gem for that matter. I suggest impressionist. It will handle the most important stuff for you and block bots. It also offers additional features you may want.
I've been running into quite an annoying issue when dealing with Rails 4 action mailer previews and factory girl. Here's an example of some of my code:
class TransactionMailerPreview < ActionMailer::Preview
def purchase_receipt
account = FactoryGirl.build_stubbed(:account)
user = account.owner
transaction = FactoryGirl.build_stubbed(:transaction, account: account, user: user)
TransactionMailer.purchase_receipt(transaction)
end
end
This could really be any action mailer preview. Lets say I get something wrong (happens every time), and there's an error. I fix the error and refresh the page. Every time this happens I get a:
"ArgumentError in Rails::MailersController#preview
A copy of User has been removed from the module tree but is still active!"
Then my only way out is to restart my server.
Am I missing something here? Any clue as to what is causing this and how it could be avoided? I've restarted my server 100 times over the past week because of this.
EDIT: It may actually be happening any time I edit my code and refresh the preview?
This answers my question:
https://stackoverflow.com/a/29710188/2202674
I used approach #3: Just put a :: in front of the offending module.
Though this is not exactly an answer (but perhaps a clue), I've had this problem too.
Do your factories cause any records to actually be persisted?
I ended up using Factory.build where I could, and stubbing out everything else with private methods and OpenStructs to be sure all objects were being created fresh on every reload, and nothing was persisting to be reloaded.
I'm wondering if what FactoryGirl.build_stubbed uses to trick the system into thinking the objects are persisted are causing the system to try and reload them (after they are gone).
Here's a snippet of what is working for me:
class SiteMailerPreview < ActionMailer::Preview
def add_comment_to_page
page = FactoryGirl.build :page, id: 30, site: cool_site
user = FactoryGirl.build :user
comment = FactoryGirl.build :comment, commentable: page, user: user
SiteMailer.comment_added(comment)
end
private
# this works across reloads where `Factory.build :site` would throw the error:
# A copy of Site has been removed from the module tree but is still active!
def cool_site
site = FactoryGirl.build :site, name: 'Super cool site'
def site.users
user = OpenStruct.new(email: 'recipient#example.com')
def user.settings(sym)
OpenStruct.new(comments: true)
end
[user]
end
site
end
end
Though I am not totally satisfied with this approach, I don't get those errors anymore.
I would be interested to hear if anyone else has a better solution.
As preface, I've followed through some tutorials (i.e. Michael Hartl's) though I'm still fairly novice. Forgive any cloudy terminology.
I am trying to build a simple application in Rails 4 that does the following:
User logs into application (currently working with sign-in-with-twitter link and routing)
get "/auth/:provider/callback" => "sessions#create"
get "/signout" => "sessions#destroy", :as => :signout
Once <% if current_user %> is true, I have the view rendering a partial where there will be a list of simple buttons. When the user clicks a button I want the application to tweet on behalf of the current_user a preset string. Ideally, I'd do this all in ruby/rails.
These button functions are where I'm getting hung up. I've read a fistful of documents but there seem to be a lot of conflicting and old answers. Here's a quick list of the ones I think are closest, though not explicit about sending a tweet from a simple button/link in a view:
http://www.sitepoint.com/ruby-social-gems-twitter/
http://richonrails.com/articles/sending-a-tweet-to-twitter
Some call for controllers, a more robust oauth setup (which I have bundle installed and connected to the dev.twitter application, though not fleshed out beyond keys), and whatever else. It's got me turned around and I'm not yet good enough to synthesize all the information. Any help and direction would be great. Below are some other files in the app that might be helpful.
class SessionsController < ApplicationController
def create
auth = request.env["omniauth.auth"]
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth)
session[:user_id] = user.id
redirect_to root_url, :notice => "Hi!"
end
def destroy
session[:user_id] = nil
redirect_to root_url, :notice => "Bye!"
end
end
And omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, '_priv', '_priv'
end
Eep! I'm the author of the second link (RichOnRails). Did you take a look at the example app included with the tutorial? It does almost exactly what you want. If the tweets are hard coded you could approach it in a couple of different ways. If you take a look at the tweets controller, you'll see it takes a parameter called 'message'. Any message passed to that create method will tweet as the current user.
def create
current_user.tweet(twitter_params[:message])
end
The easiest (but not necessarily best) way to adapt this to fit your needs is to have a form for each tweet, and do a hidden field with the message you wish to tweet. The button becomes a submit for that particular form (you can add remote: true if you want to keep the page from refreshing, then use a bit of javascript to update the UI elements). Hope this helps.
I have a bit of code that is causing my page to load pretty slow (49 queries in 128 ms). This is the landing page for my site -- so it needs to load snappily.
The following is my views.py that creates a feed of latest updates on the site and is causing the slowest load times from what I can see in the Debug toolbar:
def product_feed(request):
""" Return all site activity from friends, etc. """
latestparts = Part.objects.all().prefetch_related('uniparts').order_by('-added')
latestdesigns = Design.objects.all().order_by('-added')
latest = list(latestparts) + list(latestdesigns)
latestupdates = sorted (latest, key = lambda x: x.added, reverse = True)
latestupdates = latestupdates [0:8]
# only get the unique avatars that we need to put on the page so we're not pinging for images for each update
uniqueusers = User.objects.filter(id__in = Part.objects.values_list('adder', flat=True))
return render_to_response("homepage.html", {
"uniqueusers": uniqueusers,
"latestupdates": latestupdates
}, context_instance=RequestContext(request))
The query that causes the most time seem to be:
latest = list(latestparts) + list(latestdesigns) (25ms)
There is another one that's 17ms (sitewide annoucements) and 25ms (adding tagged items on each product feed item) respectively that I am also investigating.
Does anyone see any ways in which I can optimize the loading of my activity feed?
You never need more than 8 items, so limit your queries. And don't forget to make sure that added in both models is indexed.
latestparts = Part.objects.all().prefetch_related('uniparts').order_by('-added')[:8]
latestdesigns = Design.objects.all().order_by('-added')[:8]
For bonus marks, eliminate the magic number.
After making those queries a bit faster, you might want to check out memcache to store the most common query results.
Moreover, I believe adder is ForeignKey to User model.
Part.objects.distinct().values_list('adder', flat=True)
Above line is QuerySet with unique addre values. I believe you ment exactly that.
It saves you performing a subuery.