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.
So, I have a situation where I need to determine something about a request before it is dispatched to any of the routes. Currently, this is implemented using several constraints that all hit the database, and I want to reduce the database hit to one. Unfortunately, doing it inline in routes.rb doesn't work, because the local variables within routes.rb don't get refreshed between requests; so if I do:
# Database work occurs here, and is then used to create comparator lambdas.
request_determinator = RequestDeterminator.new(request)
constraint(request_determinator.lambda_for(:ninja_requests)) do
# ...
end
constraint(request_determinator.lambda_for(:pirate_requests)) do
# ...
end
This works great on the first request, but then subsequent requests get routed as whatever the first request was. (D'oh.)
My next thought was to write a Rack middleware to add the "determinator" to the env hash, but there are two problems with this: first, it doesn't seem to be sticking in the hash at all, and specs don't even go through the Rack middleware, so there's no way to really test it.
Is there a simple mechanism I'm overlooking where I can insert, say, a hook for ActionDispatch to add something to the request, or just to say to Rails routing: "Do this before routing?"
I am using Rails 3.2 and Ruby 1.9.
One way to do this would be to store your determinator on the request's env object (which you have since ActionDispatch::Request is a subclass of Rack::Request):
class RequestDeterminator
def initialize(request)
#request = request
end
def self.for_request(request)
request.env[:__determinator] ||= new(request)
end
def ninja?
query_db
# Verify ninjaness with #request
end
def pirate?
query_db
# Verify piratacity with #request
end
def query_db
#result ||= begin
# Some DB lookup here
end
end
end
constraint lambda{|req| RequestDeterminator.for_request(req).ninja? } do
# Routes
end
constraint lambda{|req| RequestDeterminator.for_request(req).pirate? } do
# Routes
end
That way, you just instantiate a single determinator which caches your DB request across constraint checks.
if you really want to intercept the request,try rack as it is the first one to handle request in any Rails app...refer http://railscasts.com/episodes/151-rack-middleware to understand how rack works....
hope it helps.
I'm trying to use the method outlined this post in conjunction with url_for to determine if the current path is in a mounted engine, but I'm having a hard time figuring out how exactly to use Journey::Path::Pattern (which is what is returned by the mounted_path method outlined in the other post).
class Rails::Engine
def self.mounted_path
route = Rails.application.routes.routes.detect do |route|
route.app == self
end
route && route.path
end
end
There doesn't seem to be too much discussion on it anywhere, aside from the official documentation, which wasn't particularly helpful. I'm sure the solution is relatively simple, and the gist of the helper method I'm trying to write is below:
def in_engine? engine
current_url.include?(engine.mounted_path)
end
Edit:
Some of my engines are mounted as subdomains and some are mounted within the app itself, preventing me from simply checking if the current subdomain is the same as the mounted path, or using path_for.
Not exactly a solution, but maybe a useful lead.
I found your question interesting, so I started delving deep inside rails source... what a scary, yet instructive trip :D
Turns out that Rails' router has a recognize method that accepts a request as argument, and yields the routes that match the request.
As the routes have an app method you can compare to your engine, and as you can have access to the request object (which takes into account the http method, subdomain, etc), if you find out how to have direct access to the router instance, you should be able to do something along the lines of :
def in_engine?(engine)
router.recognize(request) do |route,*|
return true if route.app == engine
end
false
end
EDIT
I think i found out, but it's late here in I have no rails app at hand to test this :(
def in_engine?(engine)
# get all engine routes.
# (maybe possible to do this directly with the engine, dunno)
engine_routes = Rails.application.routes.set.select do |route|
route.app == engine
end
!!engine_routes.detect{ |route| route.matches?(request) }
end
EDIT
also, maybe a simpler workaround would be to do this :
in your main app
class ApplicationController < ActionController::Base
def in_engine?(engine)
false
end
helper_method :in_engine?
end
then in your engine's application controller
def in_engine?(engine)
engine == ::MyEngine
end
helper_method :in_engine?
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
I'm building a CMS with various modules (blog, calendar, etc.) using Rails 2.3. Each module is handled by a different controller and that works just fine.
The only problem I have is with the root URL. Depending on the configuration chosen by the user, this default URL should show a different module i.e. a different controller, but the only way I have to determine the correct controller is by checking the database for what "default" module is to be shown.
For the moment I'm using a specific "root" controller which checks the database and redirects to the correct controller. However I'd prefer the URL not to be changed, which means I want to invoke the correct controller from the very same request.
I've tried using Rails Metal to fetch this info and manually calling the controller I want but I'm thinking I may be reinventing the wheel (identify the request path to choose the controller, manage session, etc.).
Any idea? Thanks a lot in advance!
This problem can be solved with some Rack middleware:
This code in lib/root_rewriter.rb:
module DefV
class RootRewriter
def initialize(app)
#app = app
end
def call(env)
if env['REQUEST_URI'] == '/' # Root is requested!
env['REQUEST_URI'] = Page.find_by_root(true).uri # for example /blog/
end
#app.call(env)
end
end
end
Then in your config/environment.rb at the bottom
require 'root_rewriter'
ActionController::Dispatcher.middleware.insert_after ActiveRecord::QueryCache, DefV::RootRewriter
This middleware will check if the requested page (REQUEST_URI) is '/' and then do a lookup for the actual path (Implementation to this is up to you ;-)). You might do good on caching this info somewhere (Cache.fetch('root_path') { Page.find... })
There are some problems with checking REQUEST_URI, since not all webservers pass this correctly. For the whole implementation detail in Rails see http://api.rubyonrails.org/classes/ActionController/Request.html#M000720 (Click "View source")
In Rails 3.2 this was what I came up with (still a middleware):
class RootRewriter
def initialize(app)
#app = app
end
def call(env)
if ['', '/'].include? env['PATH_INFO']
default_thing = # Do your model lookup here to determine your default item
env['PATH_INFO'] = # Assemble your new 'internal' path here (a string)
# I found useful methods to be: ActiveModel::Naming.route_key() and to_param
end
#app.call(env)
end
end
This tells Rails that the path is different from what was requested (the root path) so references to link_to_unless_current and the like still work well.
Load the middleware in like so in an initialiser:
MyApp::Application.config.middleware.use RootRewriter