Displaying GET request from HTTparty on view - ruby-on-rails

I currently have a simple ruby file named example.rb. How can I make a view that allows a user to submit information into a form and then have the information from the GET request returned to them? I understand how to use these requests from the console, but not from the front-end.
Resources on this topic would also be greatly appreciated.
require 'rubygems'
require 'httparty'
class StackExchange
include HTTParty
base_uri 'api.stackexchange.com'
def initialize(service, page)
#options = {query: {site: service}}
end
def questions
self.class.get('/2.2/questions', #options)
end
def users
self.class.get('/2.2/users', #options)
end
end
stack_exchange = StackExchange.new('stackoverflow',1)
puts stack_exchange.users

Make sure the HTTParty gem is in your application's Gemfile.
Take example.rb and put it in /app/models/stack_exchange.rb — yes the file name does matter[0] (this isn't the purists place to put this, but for beginners it's fine and perfectly acceptable). Remove the code at the bottom you're using to test it as well.
in routes.rb add this route: get '/test' => 'application#test'
in your application_controller.rb add this method:
def test
stack_client = StackExchange.new('stackoverflow', 1)
#users = stack_client.users
end
in app/views/application/test.html.erb put the following:
<% #users.each do |user| %><%=user.inspect%><br/><br/><% end %>
Note: I would otherwise recommend adding views to ApplicationController but because I don't know anything about your application, I'll default to it.
hit http://localhost:3000/test and you should see the expected result.
[0] Rails does a lot "magic" under the scenes — it's really not magic but metaprogramming — where it tries to assume a lot of things about your application structure and naming conventions. If your class was named Stackexchange (note the lowercase e), stackexchange.rb would be automatically "mapped" to the class Stackexchange. More info: http://guides.rubyonrails.org/autoloading_and_reloading_constants.html

Related

Stuck in parsing URL and work with it in a view

Trying to parse a URL with this format http://landing.com?data=123 - I'm been able to get the Data through irb like:
require "addressable/uri"
uri = Addressable::URI.parse("http://landing.com?data=123")
uri.query_values['data']
=> '123'
But I'm stuck on how to interact with that 'data' within a Rails view. I have tried including it in Controller (pages_controller.rb in my sample) like:
class PagesController < InheritedResources::Base
def test
uri = Addressable::URI.parse("<%= request.original_url %>")
u = uri.query_values['data']
end
end
But no idea how can I extract that piece of data to be used within my Views. Any guidance on this?
If I open one of the views like where I call that 'test' method - I'm getting uninitialized constant PagesController::Addressable but made sure it's in my enviroment with gem which addressable/uri
Controllers have a lot of the query information already parsed. You can access it with params. In that case, you can use
u = params[:data]
As Sophie Déziel said, if it's under an app request, you can access to your query values through params hash. params is present in your controllers and views.
If you are talking about hardcoded URLs or URLS that you get from 3rd party sources, you will nee to create an instance variable in your controller (#u = ...) to be available in your views.
Note that you're not supposed to call action methods in your views, they are 'invoked' by Rails framework.
# controller
def my_action
# .....
#u = uri.query_values['data']
end
# view
<%= #u %>

Embedding serverside Ruby to opal files in rails

Is it possible to embed serverside ruby to Opal .js.rb files?
In other words I need to access for example User.find(1) in some opal file in Rails, like one would do with .erb.
Works like this out of the box in Reactive-Record. Otherwise you have to build some kind of api between the client + v8 prerendering engine and the server to get the data (which is what reactive-record does.) You can look at reactive-ruby and reactive-record's code in the files labeled "isomorphic..." See the React.rb Reactive-Ruby branch readme for more info.
It sounds like you're looking for a way to pass data to the client in the initial request. If you load the data in the server app and just want to pass it along, you can look into the gon gem.
TL;DR version
Here's enough to get your feet wet
# Gemfile
gem 'gon'
# app/controllers/my_controller.rb
class MyController < ApplicationController
before_action { gon.push current_user: User.find(session[:user_id]) }
end
# app/views/layouts/application.html.erb
<head>
<%= include_gon %>
</head>
# app/assets/javascripts/application.rb
require 'native' # To be able to convert JS objects to hashes.
current_user = User.new(Hash.new(`gon.current_user`))
The explanation:
The gon gem sets up a hash that will be JS-objectified and placed into your JS runtime in the browser. For more info on that, see the Railscast about it (it's still mostly relevant, not much has changed).
In the browser, that JS object is also named gon, so gon.current_user would be whatever you set in gon.push current_user: some_user in the controller action (or before_action callback).
But we'll need to convert that from a JS object to a Ruby hash using Hash.new(gon.current_user), then you can pass that resulting hash to a User.new call.
The important part: you'll need to have a User class in your Opal app. If your Rails app sets up User an ActiveRecord model, you won't be able to reuse it, so you'll need to create a new one, but if it's just a PORO (plain-old Ruby object), you can add the following line to an initializer:
Opal.append_path Rails.root.join('app', 'shared')
Then you can put Ruby code that you want available in both your Rails app and your Opal app in app/shared. For example, you could do this in app/shared/user.rb:
class User
attr_reader :id, :name, :email
def initialize(attributes={})
attributes.each do |attr, value|
instance_variable_set "##{attr}", value
end
end
end

Dynamic Routes Rails 4, taken from db

Frustrating, I can't find an eligible solution for my problem.
In my Rails 4 app, I want to give my users the possibility to add their own custom post types to their sites. Like:
www.example.com/houses/address-1
www.example2.com/sports/baseball
Both would work, but only for the linked sites. Sports and houses would be the (RESTful) post types, taken from the db, added by users.
I have been struggling to find a elegant solution to accomplish this. I found http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4 but that feels kinda hacky and I'm not sure if reloading the routes works in production, I'm getting signals that it won't.
I'd say I have to use routes constraints http://guides.rubyonrails.org/routing.html#advanced-constraints but I don't have a clue how to approach this.
To be clear, I have no problem with the site setting stuff, the multi tenancy part of my app is fully functional (set in Middleware, so the current site is callable in the routes.rb file). My issue is with the (relative) routes, and how they could be dynamically set with db records.
Any pointers much appreciated.
I think route constraints don't work for you because your domain is a variable here. Instead, you should be examining the request object.
In your ApplicationController, you could define a method that would be called before any action, like so:
class ApplicationController < ActionController::Base
before_action :identify_site
def identify_site
#site = Site.where(:domain => request.host).first
end
end
As you scale, you could use Redis for your domains so you're not making an expensive SQL call on each request.
Then you can just add the #site as a parameter to whatever call you're making. I'm assuming you're doing some sort of "Post" thing, so I'll write some boilerplate code:
class PostController < ApplicationController
def show
#post = Post.where(:site => #site, :type => params[:type], :id => params[:id])
end
end
Just write your routes like any other regular resource.

Can a mobile mime type fall back to "html" in Rails?

I'm using this code (taken from here) in ApplicationController to detect iPhone, iPod Touch and iPad requests:
before_filter :detect_mobile_request, :detect_tablet_request
protected
def detect_mobile_request
request.format = :mobile if mobile_request?
end
def mobile_request?
#request.subdomains.first == 'm'
request.user_agent =~ /iPhone/ || request.user_agent =~ /iPod/
end
def detect_tablet_request
request.format = :tablet if tablet_request?
end
def tablet_request?
#request.subdomains.first == 't'
request.user_agent =~ /iPad/
end
This allows me to have templates like show.html.erb, show.mobile.erb, and show.tablet.erb, which is great, but there's a problem: It seems I must define every template for each mime type. For example, requesting the "show" action from an iPhone without defining show.mobile.erb will throw an error even if show.html.erb is defined. If a mobile or tablet template is missing, I'd like to simply fall back on the html one. It doesn't seem too far fetched since "mobile" is defined as an alias to "text/html" in mime_types.rb.
So, a few questions:
Am I doing this wrong? Or, is there a better way to do this?
If not, can I get the mobile and tablet mime types to fall back on html if a mobile or tablet file is not present?
If it matters, I'm using Rails 3.0.1. Thanks in advance for any pointers.
EDIT: Something I forgot to mention: I'll eventually be moving to separate sub-domains (as you can see commented out in my example) so the template loading really needs to happen automatically regardless of which before_filter has run.
Possible Duplicate of Changing view formats in rails 3.1 (delivering mobile html formats, fallback on normal html)
However, I struggled with this exact same problem and came up with a fairly elegant solution that met my needs perfectly. Here is my answer from the other post.
I think I've found the best way to do this. I was attempting the same thing that you were, but then I remembered that in rails 3.1 introduced template inheritance, which is exactly what we need for something like this to work. I really can't take much credit for this implementation as its all laid out there in that railscasts link by Ryan Bates.
So this is basically how it goes.
Create a subdirectory in app/views. I labeled mine mobile.
Nest all view templates you want to override in the same structure format that they would be in the views directory. views/posts/index.html.erb -> views/mobile/posts/index.html.erb
Create a before_filter in your Application_Controller and do something to this effect.
before_filter :prep_mobile
def is_mobile?
request.user_agent =~ /Mobile|webOS|iPhone/
end
def prep_mobile
prepend_view_path "app/views/mobile" if is_mobile?
end
Once thats done, your files will default to the mobile views if they are on a mobile device and fallback to the regular templates if a mobile one is not present.
You need to do several things to wire this up, but the good news is that Rails 3 actually makes this a lot simpler than it used to be, and you can let the router do most of the hard work for you.
First off, you need to make a special route that sets up the correct mime type for you:
# In routes.rb:
resources :things, :user_agent => /iPhone/, :format => :iphone
resources :things
Now you have things accessed by an iphone user agent being marked with the iphone mime type. Rails will explode at you for a missing mime type though, so head over to config/initializers/mime_types.rb and uncomment the iphone one:
Mime::Type.register_alias "text/html", :iphone
Now you're mime type is ready for use, but your controller probably doesn't yet know about your new mime type, and as such you'll see 406 responses. To solve this, just add a mime-type allowance at the top of the controller, using repsond_to:
class ThingsController < ApplicationController
respond_to :html, :xml, :iphone
Now you can just use respond_to blocks or respond_with as normal.
There currently is no API to easily perform the automatic fallback other than the monkeypatch or non-mime template approaches already discussed. You might be able to wire up an override more cleanly using a specialized responder class.
Other recommended reading includes:
https://github.com/plataformatec/responders
http://www.railsdispatch.com/posts/rails-3-makes-life-better
Trying removing the .html from the .html.erb and both iPhone and browser will fallback to the common file.
I have added a new answer for version 3.2.X. This answer is valid for <~ 3.0.1.
I came to this question while looking to be able to have multiple fallbacks on the view. For example if my product can be white-labeled and in turn if my white-label partner is able to sell sponsorship, then I need a cascade of views on every page like this:
Sponsor View: .sponsor_html
Partner View: .partner_html
Default View: .html
The answer by Joe, of just removing .html works (really well) if you only have one level above the default, but in actual application I needed 5 levels in some cases.
There did not seem to be anyway to implement this short of some monkey patching in the same vein as Jeremy.
The Rails core makes some fairly wide ranging assumptions that you only want one format and that it maps to a single extension (with the default of NO extension).
I needed a single solution that would work for all view elements -- layouts, templates, and partials.
Attempting to make this more along the lines of convention I came up with the following.
# app/config/initializers/resolver.rb
module ActionView
class Base
cattr_accessor :extension_fallbacks
##extension_fallbacks = nil
end
class PathResolver < Resolver
private
def find_templates_with_fallbacks(name, prefix, partial, details)
fallbacks = Rails.application.config.action_view.extension_fallbacks
format = details[:formats].first
unless fallbacks && fallbacks[format]
return find_templates_without_fallbacks(name, prefix, partial, details)
end
deets = details.dup
deets[:formats] = fallbacks[format]
path = build_path(name, prefix, partial, deets)
query(path, EXTENSION_ORDER.map {|ext| deets[ext] }, details[:formats])
end
alias_method_chain :find_templates, :fallbacks
end
end
# config/application.rb
config.after_initialize do
config.action_view.extension_fallbacks = {
html: [:sponsor_html, :partner_html, :html],
mobile: [:sponsor_mobile, :partner_mobile, :sponsor_html, :partner_html, :html]
}
# config/initializers/mime_types.rb
register_alias 'text/html', :mobile
# app/controllers/examples_controller.rb
class ExamplesController
respond_to :html, :mobile
def index
#examples = Examples.all
respond_with(#examples)
end
end
Note: I did see the comments around alias_method_chain, and initially did make a call to super at the appropriate spot. This actually called ActionView::Resolver#find_templates (which raises a NotImplemented exception) rather than the ActionView::PathResolver#find_templates in some cases. I wasn't patient enough to track down why. I suspect its because of being a private method.
Plus, Rails, at this time, does not report alias_method_chain as deprecated. Just that post does.
I do not like this answer as it involves some very brittle implementation around that find_templates call. In particular the assumption that you only have ONE format, but this is an assumption made all over the place in the template request.
After 4 days of trying to solve this and combing through the whole of the template request stack its the best I can come up with.
The way that I'm handling this is to simply skip_before_filter on those requests that I know I want to render the HTML views for. Obviously, that will work with partials.
If your site has a lot of mobile and/or tablet views, you probably want to set your filter in ApplicationController and skip them in subclasses, but if only a few actions have mobile specific views, you should only call the before filter on those actions/controllers you want.
If your OS has symlinks you could use those.
$ ln -s show.html.erb show.mobile.erb
I am adding another answer now that we have updated to 3.2.X. Leaving the old answer as it was in case someone needs that one. But, I will edit it to direct people to this one for current versions.
The significant difference here is to make use of the "new" (since 3.1) availability of adding in custom path resolvers. Which does make the code shorter, as Jeroen suggested. But taken a little bit further. In particular the #find_templates is no longer private and it is expected that you will write a custom one.
# lib/fallback_resolver.rb
class FallbackResolver < ::ActionView::FileSystemResolver
def initialize(path, fallbacks = nil)
#fallback_list = fallbacks
super(path)
end
def find_templates(name, prefix, partial, details)
format = details[:formats].first
return super unless #fallback_list && #fallback_list[format]
formats = Array.wrap(#fallback_list[format])
details_copy = details.dup
details_copy[:formats] = formats
path = Path.build(name, prefix, partial)
query(path, details_copy, formats)
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
append_view_path 'app/views', {
mobile: [:sponsor_mobile, :mobile, :sponsor_html, :html],
html: [:sponsor_html, :html]
}
respond_to :html, :mobile
# config/initializers/mime_types.rb
register_alias 'text/html', :mobile
Here's a simpler solution:
class ApplicationController
...
def formats=(values)
values << :html if values == [:mobile]
super(values)
end
...
end
It turns out Rails (3.2.11) adds an :html fallback for requests with the :js format. Here's how it works:
ActionController::Rendering#process_action assigns the formats array from the request (see action_controller/metal/rendering.rb)
ActionView::LookupContext#formats= gets called with the result
Here's ActionView::LookupContext#formats=,
# Override formats= to expand ["*/*"] values and automatically
# add :html as fallback to :js.
def formats=(values)
if values
values.concat(default_formats) if values.delete "*/*"
values << :html if values == [:js]
end
super(values)
end
This solution is gross but I don't know a better way to get Rails to interpret a request MIME type of "mobile" as formatters [:mobile, :html] - and Rails already does it this way.
Yes, I'm pretty sure this is the right way to do this in rails. I've defined iphone formats this way before. That's a good question about getting the format to default back to :html if a template for iphone doesn't exist. It sounds simple enough, but I think you'll have to add in a monkeypath to either rescue the missing template error, or to check if the template exists before rendering. Take a look a the type of patches shown in this question. Something like this would probably do the trick (writing this code in my browser, so more pseudo code) but throw this in an initializer
# config/initializers/default_html_view.rb
module ActionView
class PathSet
def find_template_with_exception_handling(original_template_path, format = nil, html_fallback = true)
begin
find_template_without_exception_handling(original_template_path, format, html_fallback)
rescue ActionView::MissingTemplate => e
# Template wasn't found
template_path = original_template_path.sub(/^\//, '')
# Check to see if the html version exists
if template = load_path["#{template_path}.#{I18n.locale}.html"]
# Return html version
return template
else
# The html format doesn't exist either
raise e
end
end
end
alias_method_chain :find_template, :exception_handling
end
end
Here is another example of how to do it, inspired by Simon's code, but a bit shorter and a bit less hacky:
# application_controller.rb
class ApplicationController < ActionController::Base
# ...
# When the format is iphone have it also fallback on :html
append_view_path ExtensionFallbackResolver.new("app/views", :iphone => :html)
# ...
end
and somewhere in an autoload_path or explicitly required:
# extension_fallback_resolver.rb
class ExtensionFallbackResolver < ActionView::FileSystemResolver
attr_reader :format_fallbacks
# In controller do append_view_path ExtensionFallbackResolver.new("app/views", :iphone => :html)
def initialize(path, format_fallbacks = {})
super(path)
#format_fallbacks = format_fallbacks
end
private
def find_templates(name, prefix, partial, details)
fallback_details = details.dup
fallback_details[:formats] = Array(format_fallbacks[details[:formats].first])
path = build_path(name, prefix, partial, details)
query(path, EXTENSION_ORDER.map { |ext| fallback_details[ext] }, details[:formats])
end
end
The above is still a hack because it is using a private API, but possibly less fragile as Simon's original proposal.
Note that you need to take care of the layout seperately. You will need to implement a method that chooses the layout based on the user agent or something similar. The will only take care of the fallback for the normal templates.
Rails 4.1 includes Variants, this is a great feature that allow you to set different views for the same mime. You can now simply add a before_action and let the variant to do the magic:
before_action :detect_device_variant
def detect_device_variant
case request.user_agent
when /iPad/i
request.variant = :tablet
when /iPhone/i
request.variant = :phone
end
end
Then, in your action:
respond_to do |format|
format.json
format.html # /app/views/the_controller/the_action.html.erb
format.html.phone # /app/views/the_controller/the_action.html+phone.erb
format.html.tablet do
#some_tablet_specific_variable = "foo"
end
end
More info here.
You can in this case for the format to html. By example you want always use the html in user show method
class UserController
def show
..your_code..
render :show, :format => :html
end
end
In this case, if you request show on User controller you render all the time the html version.
If you want render JSON too by example you can made some test about your type like :
class UserController
def show
..your_code..
if [:mobile, :tablet, :html].include?(request.format)
render :show, :format => :html
else
respond_with(#user)
end
end
end
I made a monkey patch for that, but now, I use a better solution :
In application_controller.rb :
layout :which_layout
def which_layout
mobile? ? 'mobile' : 'application'
end
With the mobile? method you can write.
So I have a different layout but all the same views, and in the mobile.html.erb layout, I use a different CSS file.
I need the same thing. I researched this including this stack overflow question (and the other similar one) as well as followed the rails thread (as mentioned in this question) at https://github.com/rails/rails/issues/3855 and followed its threads/gists/gems.
Heres what I ended up doing that works with Rails 3.1 and engines. This solution allows you to place the *.mobile.haml (or *.mobile.erb etc.) in the same location as your other view files with no need for 2 hierarchies (one for regular and one for mobile).
Engine and preparation Code
in my 'base' engine I added this in config/initializers/resolvers.rb:
module Resolvers
# this resolver graciously shared by jdelStrother at
# https://github.com/rails/rails/issues/3855#issuecomment-5028260
class MobileFallbackResolver < ::ActionView::FileSystemResolver
def find_templates(name, prefix, partial, details)
if details[:formats] == [:mobile]
# Add a fallback for html, for the case where, eg, 'index.html.haml' exists, but not 'index.mobile.haml'
details = details.dup
details[:formats] = [:mobile, :html]
end
super
end
end
end
ActiveSupport.on_load(:action_controller) do
tmp_view_paths = view_paths.dup # avoid endless loop as append_view_path modifies view_paths
tmp_view_paths.each do |path|
append_view_path(Resolvers::MobileFallbackResolver.new(path.to_s))
end
end
Then, in my 'base' engine's application controller I added a mobile? method:
def mobile?
request.user_agent && request.user_agent.downcase =~ /mobile|iphone|webos|android|blackberry|midp|cldc/ && request.user_agent.downcase !~ /ipad/
end
And also this before_filter:
before_filter :set_layout
def set_layout
request.format = :mobile if mobile?
end
Finally, I added this to the config/initializers/mime_types.rb:
Mime::Type.register_alias "text/html", :mobile
Usage
Now I can have (at my application level, or in an engine):
app/views/layouts/application.mobile.haml
and in any view a .mobile.haml instead of a .html.haml file.
I can even use a specific mobile layout if I set it in any controller:
layout 'mobile'
which will use app/views/layouts/mobile.html.haml (or even mobile.mobile.haml).
I solved this problem by using this before_filter in my ApplicationController:
def set_mobile_format
request.formats.unshift(Mime::MOBILE) if mobile_client?
end
This puts the mobile format to the front of the list of acceptable formats. So, the Resolver prefers .mobile.erb templates, but will fall back to .html.erb if no mobile version is found.
Of course, for this to work you need to implement some kind of #mobile_client? function and put Mime::Type.register_alias "text/html", :mobile into your config/initializers/mime_types.rb

In Rails, a Sweeper isn't getting called in a Model-only setup

I'm working on a Rails app, where I'm using page caching to store static html output. The caching works fine. I'm having trouble expiring the caches, though.
I believe my problem is, in part, because I'm not expiring the cache from my controller. All of the actions necessary for this are being handled within the model. This seems like it should be doable, but all of the references to Model-based cache expiration that I'm finding seem to be out of date, or are otherwise not working.
In my environment.rb file, I'm calling
config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
And I have, in the /sweepers folder, a LinkSweeper file:
class LinkSweeper < ActionController::Caching::Sweeper
observe Link
def after_update(link)
clear_links_cache(link)
end
def clear_links_cache(link)
# expire_page :controller => 'links', :action => 'show', :md5 => link.md5
expire_page '/l/'+ link.md5 + '.html'
end
end
So ... why isn't it deleting the cached page when I update the model? (Process: using script/console, I'm selecting items from the database and saving them, but their corresponding pages aren't deleting from the cache), and I'm also calling the specific method in the Link model that would normally invoke the sweeper. Neither works.
If it matters, the cached file is an md5 hash off a key value in the Links table. The cached page is getting stored as something like /l/45ed4aade64d427...99919cba2bd90f.html.
Essentially, it seems as though the Sweeper isn't actually observing the Link. I also read (here) that it might be possible to simply add the sweeper to config.active_record.observers in environment.rb, but that didn't seem to do it (and I wasn't sure if the load_path of app/sweepers in environment.rb obviated that).
So I've tried a number of different approaches, to see what works, and what doesn't.
Again, to summarize the situation: My goal is to expire cached pages when an object updates, but to expire them without relying on a Controller action. Conventional sweepers use a line in the controller to notify the sweeper that it needs to function. In this case, I can't use a line in the controller, as the update is happening within the model. Normal sweeper tutorials aren't working, as they presume that your main interaction with the database object is through the controller.
If, in reading this, you see a way to tighten up my code, please comment and let me know.
First, let's look at the things that DO work, in case you're stuck on this, too, and need help.
Of all the things I tried, the only thing that really seemed to work was to declare an after_update command in the Observer for the model. In that command, I used the explicit command for the expire_page action, and included a path that had been declared in routes.rb.
So. This works:
In config/routes.rb:
map.link 'l/:md5.:format', :controller => 'links', :action => 'show'
In app/models/link_observer.rb:
def after_update(link)
ActionController::Base.expire_page(app.link_path(:md5 => link.md5))
end
Note that that "md5" is specific to my app. You might want to use :id or some other unique identifier.
I also found that declaring that ActionController::Base... line from the method in the model that's doing the updating worked. That is, within Link.rb, in the method that's actually updating the database, if I just stuck that whole line in, it worked. But since I might want to expire that page cache on other methods in the future, I'd rather have it extracted into the Observer.
Now, let's look at some things that DID NOT work, in case you're Googling around for this.
Calling "expire_page(...)" within the after_update(link) method within link_observer.rb did not work, as it returned an "undefined method `expire_page'" error
Creating a Sweeper file that observed the Model did not work. I couldn't find any error codes, but it just seemed to not even be aware that it had a job to do. This was after explicitly calling "config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )" within environment.rb. Just in case I fat-fingered something in that code, here it is:
class LinkSweeper < ActionController::Caching::Sweeper
observe Link
def after_update(link)
clear_links_cache(link)
end
def clear_links_cache(link)
# DID NOT WORK expire_page :controller => 'links', :action => 'show', :md5 => link.md5
# DID NOT WORK expire_page '/l/'+ link.md5 + '.html'
# DID NOT WORK ActionController::Base.expire_page(app.link_path(:md5 => link.md5))
end
end
That above example had the link_sweeper.rb file in a directory, /app/sweepers. I also tried putting link_sweeper.rb within the app/models directory, and tried calling it with the config.active_record.observers command in environment.rb:
config.active_record.observers = :link_observer, :link_sweeper
But that didn't work, either.
So, yeah. It's quite possible that one of these methods would work, and that I messed up something in the code. But I think I did everything by the book.
Ultimately, to summarize: Rather than using a Sweeper to expire page caching, you want to set up an after_ callback in the model's Observer. You'll want to use the explicit path to the Base.expire_page method:
def after_update(<model>) # where <model> is the name of the model you're observing
ActionController::Base.expire_page(app.<model>_path(:id => <model>.id)) # where <model> is the name of the model you're observing
end
Hopefully this will help someone else down the road. Again, if you see anywhere in my not-working code where I should have done something differently, please let me know. If you see something in my working code that can be tighter, please let me know that, too.
Just a note: you can use cache_sweeper in ApplicationController.
class ApplicationController < ActionController::Base
cache_sweeper :my_sweeper
end
class MySweeper < ActionController::Caching::Sweeper
observe MyModel
def after_update(my_model)
expire_page(...)
end
end
I was experiencing the same problem when trying to do fragment caching (rails 3). Couldn't get the sweeper to observe, so I settled for the solution to make it an AR Observer as described above and calling ApplicationController.new.expire_fragment(...).
I did get this working. The only slight difference in my setup is that the sweeper is part of a Rails engine; which accounts for slight differences (loading the sweeper file with a require in the engine's init instead of adding it to the load path in environment.rb, etc).
So, the sweeper is loaded in the init.rb of the engine like this:
require File.join(File.dirname(__FILE__), 'app', 'sweepers', cached_category_count_sweeper')
I called it a sweeper because it "sweeps" the cache, but I guess its just an observer on the model:
class CachedCategoryCountSweeper < ActiveRecord::Observer
observe CategoryFeature
def before_save(cf)
expire_cache(cf.category_id_was) if cf.category_id_changed?
end
def after_save(cf)
expire_cache(cf.category_id)
end
def after_destroy(cf)
expire_cache(cf.category_id)
end
def expire_cache(c)
ApplicationController.expire_page("/categories/#{c}/counts.xml") if !c.nil?
end
end
Frankly, I don't like having to hard-code the path, but I tried adding:
include ActionController:UrlWriter
and then using the path method, but it only worked for me in development. It didn't work in production, because my production server uses a relative url root (instead of virtual hosts) and the internal method "page_cache_path" would consistently get the file path wrong so it couldn't expire.
Since this is an observer, I added to the environment.rb:
config.active_record.observers = :cached_category_count_sweeper
Finally the controller that uses the cache (doesn't expire it, that is done through the model observer):
class CachedCategoryCountsController < ApplicationController
caches_page :index
# GET /cached_category_counts.xml
def index
...
end
end
Anyhow, hope this helps.
Andres Montano
I've been able to get it to work, by way of adding
ActionController::Base.expire_page(app.link_path(:md5 => #link.md5))
to the method in the Model itself that's updating the database. This feels somewhat hacky, though, and I'd love to know if anyone can explain why it's not working with the normal sweeper setup, and if there's a more elegant way to handle this.
That snippet of code (apart from customizations I put in for my own app) came from this post on ruby-forum.com.
I wrote a bit about this topic here: Rails Cache Sweeper Confusion. Would love to hear your opinions.
Based on #moiristo and #ZoogieZork 's answers, I am guessing this would work (untested).
class LinkSweeper < ActiveRecord::Observer
include ActionController::Caching::Pages
# or if you want to expire fragments
#include ActionController::Caching::Fragments
observe Link
def after_update(link)
expire_page( ... )
#expire_fragment( ... )
end
end

Resources