I want to create a CMS like site where the user starts off with a some generic pages, i.e.
homepage
about
contact
etc
and from there can add child pages dynamically, for example
homepage
articles
article1
something
something-else
article2
about
contact
etc
To achieve this I'm planning on using some kind of self-referential association like
class Page < ActiveRecord::Base
belongs_to :parent, :class_name => 'Page'
has_many :children, :class_name => 'Page'
end
The one thing I'm struggling with is the route generation. Because pages can be added on the fly I need to dynamically generate routes for these pages and there is no way of knowing how many levels deep a page may be nested
So if I start off with the homepage:
/
and then start adding pages i.e.
/articles/article1/something/something-else/another-thing
How can something like that be achieved with the rails routing model?
Once you have some way to generate the URL string for your Page records (and I'll leave that part up to you), you can just map every page in config/routes.rb:
Page.all.each do |page|
map.connect page.url, :controller => 'pages', :action => 'show', :id => page
end
And have an observer hook the page model to reload routes when something changes:
class PageObserver < ActiveRecord::Observer
def reload_routes(page)
ActionController::Routing::Routes.reload!
end
alias_method :after_save, :reload_routes
alias_method :after_destroy, :reload_routes
end
Don't forget to edit config/environment.rb to load the observer:
# Activate observers that should always be running
config.active_record.observers = :page_observer
One solution to this prob is to dynamically load routes from hooks on your models. From example, a snippet from the Slug model on my site:
class Slug < ActiveRecord::Base
belongs_to :navigable
validates_presence_of :name, :navigable_id
validates_uniqueness_of :name
after_save :update_route
def add_route
new_route = ActionController::Routing::Routes.builder.build(name, route_options)
ActionController::Routing::Routes.routes.insert(0, new_route)
end
def remove_route
ActionController::Routing::Routes.routes.reject! { |r| r.instance_variable_get(:#requirements)[:slug_id] == id }
end
def update_route
remove_route
add_route
end
def route_options
#route_options ||= { :controller => navigable.controller,
:action => navigable.action,
:navigable_id => navigable_id,
:slug_id => id }
end
end
This inserts the route at top priority (0 in the routing array in memory) after it has been saved.
Also, it sounds like you should be using a tree management plugin and like awesome nested set or better nested set to manage the tree for your site.
You have to parse the route yourself
map.connect '*url', :controller => 'pages', :action => 'show'
Now you should have a params[:url] available in your action that is the request path as an array separated by the slashes. Once you have those strings its a simple matter to find the models you need from there.
That was from memory, and it's been a long while. Hope it works for you.
Look at RadiantCMS sources, they implement that functionality as far as i understand their self description.
I've implemented a similar functionality into a Rails gem, using self referential associations and a tree like js interface for reordering and nesting the "pages".
Templating language and authentication/authorization are left for the developer to implement.
https://github.com/maca/tiny_cms
Related
I have a Consultant model, where i have multiple consultant types (lawyers, doctors, psychologists etc.) listed on different websites, all handled from the same rails project.
I would like to have the consultant type as a part of the url, but have a hard time figuring out how, since it is dynamic based on domain/consultant type.
I am hoping for a solution to do a standard link:
=link_to consultant.name, consultant
without any specific link-config, so I can re-use templates across multiple consultant-websites.
Urls should be like this:
a-domain.com/doctor/doctor-name
b-domain.com/lawyer/lawyer-name
What I've tried so far, and used in the domain-specific templates (i know it is an ugly solution):
routes.rb
get 'lawyer/:slug' => 'consultants#show', as: :lawyer_consultant
get 'doctor/:slug' => 'consultants#show', as: :doctor_consultant
_consultant.html.haml for a-domain.com
= link_to consultant.name, lawyer_consultant_path(consultant)
I know the easy solution would just be this;
get 'consultant/:slug' => 'consultants#show', as: :consultant
But i want the url to be specific.
And the constraints: {host: a-domain.com} unfortunately does not allow for domain-specific routing, since only one as: :consultant can exist in routes.rb.
Routes don't actually have anything to do with your models. Your routes are the external REST API of your application while your models are an internal implementation detail.
I would just set the routes up as:
resources :doctors,
:lawyers,
only: :index
This just describes RESTful resources in your application like any other. Your routes should neither know or care that a doctor is a kind of consultant - its just a thing that can be routed to. Nor should it care that you're using slugs, to the router :id is just some kind of identifier.
The only actual connection between routes and models are the polymorphic routing helpers which basically just look up the the name of routing helper method to call based on convention over configuration:
irb(main):005:0> app.polymorphic_path("doctors") # doctors_path
=> "/doctors"
irb(main):006:0> app.polymorphic_path("doctor", id: 1) # doctor_path(1)
=> "/doctors/1"
irb(main):006:0> app.polymorphic_path(Doctor.new) # doctors_path
=> "/doctors"
irb(main):006:0> app.polymorphic_path(Doctor.find(1)) # doctor_path(1)
=> "/doctors/1"
When you pass a model instance Rails 'calls model_name.route_key on the model instance and then will determine if its singular or plural by checking if the model has been persisted.
If you want the polymorphic routing helpers to "just work" one solution is using Single Table Inheritance:
class AddTypeToConsultants < ActiveRecord::Migration[6.1]
def change
add_column :consultants, :type, :string
end
end
class Doctor < Consultant
end
class Lawyer < Consultant
end
When generating links you won't actually have to care about the type:
<%= link_to consultant.name, consultant %>
When you pass an instance of Doctor it will use doctor_path and when you pass an instance of Lawyer you get lawyer_path.
It also works for forms:
<%= form_with model: consultant do |f| %>
# ...
<% end %>
You can also acheive the same thing with the decorator pattern if STI isn't your cup of tea.
class DoctorDecorator < SimpleDelegator
def to_model
self
end
def model_name
ActiveModel::Name.new(Consultant, nil, "Doctor")
end
end
doctor = DoctorDecorator.new(Consultant.find(1))
polymorphic_path(doctor) # /doctors/1
I have an store application, where I need to make custom routing system where URL stores categories for products. For example, http://example.com/languages/ruby/rails will display category#show named 'rails', that has parent named 'ruby', that has parent named 'languages' and and URL of http://example.com/languages/ruby/rails/store will display product in this category.
Currently I have:
category.rb
belongs_to :parent, class_name: 'Category'
has_many :categories, foreign_key: :parent_id
has_many :products
routes.rb
resources :categories, :path => '', :only => [:index, :show] do
resources :products, :path => '', :only => [:show]
end
root :to => 'products#index'
but it still stacks up to 2, e.g. URL http://example.com and http://example.com/languages shows list of categories/subcategories, but http://example.com/languages/ruby have params: {"action"=>"show", "controller"=>"products", "category_id"=>"language", "id"=>"ruby"}
Removing products from routes does not help at all - then it just says that No route matches [GET] "/language/ruby", although I assume It might cause need for extra check if current URL point on category or product later on.
Also I tried get '*categories/:id', to: 'category#show' variations
+ I am using friendly_id gem so that path do not look like http://example.com/2/54/111/6
I just want to find out what is the best ruby on rails solution for this kind of situations, when you need search engine optimizations + endless (e.g. no way to define how deep such recursion can go) nested resources that nest themselves (including fact that category/language/category/ruby/category/rails just looks ugly).
Note: most information I used is taken from Stack Overflow and railscasts.com (including pro/revised episodes), so mentioning a good source with information like this will be great too.
I solved this myself recently with a CMS I built on Rails recently. I basically construct the routes dynamically at runtime from the database records. I wrote this blog post on the strategy:
http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4
The core of the solution (adapting the blog post above) is simply iterate over the database records and construct the routes needed for each category. This is the main class for doing that:
class DynamicRouter
def self.load
Website::Application.routes.draw do
Category.all.each do |cat|
get cat.route,
to: "categories#show",
defaults: { id: cat.id },
as: "#{cat.routeable_name}_#{cat.name}"
end
end
end
def self.reload
Website::Application.routes_reloader.reload!
end
end
For the above, the Category model should implement a "routeable_name" method which simply gives an underscored version of the category name that uniquely names that category's route (its not strictly necessary, but helps when doing "rake routes" to see what you have). and the #route method constructs the full route to the category. Notice the defaults which sets the ID param for the category. This makes the controller action a very simple lookup on the category's ID field like so:
class CategoryController < ApplicationController
def show
#category = Category.find(params[:id])
end
end
In application user can enter new post which contain title, content of the post and category of post. So creating new post will be through some simple html form with few fields. Now i don't know where to put logic for creating new post for following reasons:
Post(or posts collection) is object which is constructed from different tables, for example.
#posts = User.joins(entries: [{storage: :vote}, :category])
.where("votes.count > ?", 0)
.select("users.username AS username,
storages.id AS storage_id,
storages.title AS title,
storages.content AS content,
votes.count AS votes,
categories.category_name AS category_name")
.order("votes.count DESC")
So when user create new post application must create new entries in different tables:
1.Create new entry in entries table. (id, user_id, category_id)
2. Create new entry in storages table.(id, title, content, entry_id)
3. Create new entry in vote table.(id, count, storage_id)
In situation where post is model i can use something like resources: posts then in posts controller through new and create i can create new post, but what in situation like this where i don't need posts controller nor post model? So, question is: which place is more appropriate to put logic for creating new post? Q1
My solution is to craete Storages controller with resource: storages, :only => [:new, :create] then through new and create of this controller to populate different tables in db? I'm forcing here only because i dont see any point of other CRUD actions here(like showing all or one storage), because i will not use storages individually but in conjunction with other tables. So from views/storages through new.html.erb and create.html.erb i can construct new post? Q2
Another solution is to create Post controller which doesn't have "corresponding" post model as i stated before. Here i'm guessing i can't use restful routes(CRUD) because there is not :id of post? I only can make manually non-restful routes like:
post 'posts/create/:title/:content/:category' => 'posts#create', :as => 'create_post' then from params[:title], params[:content] and params[:category] to populate other tables. Q3
Im new to rails so dont yell please :D
This sound like a call for nested forms, its covered in a screen cast
here.
You use the resources of the top model, in this case Entry.
and drill down to the 3rd model.
Simple sample of what to do is bellow.
Model should look like so,
entry.rb
class Entry < ActiveRecord::Base
has_many :storages, :dependent => :destroy
accepts_nested_attributes_for :storages, :allow_destroy => true
end
storage.rb
class Storage < ActiveRecord::Base
belongs_to :entry
has_many :votes, :dependent => :destroy
accepts_nested_attributes_for :votes, :allow_destroy => true
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :storage
end
and the form should look like so, in simple_form style
<%= simple_form_for #entry do |f| %>
<%= f.simple_fields_for :storages do |storage_fields| %>
<%= storage_fields_for :votes do |vote_fields| %>
<% end %>
<% end %>
<% end %>
and if you have all the models set up, you shouldn't have to do anything to the controller.
This approach is also nice because you can add multiple storages and votes ajax style(without reloading the page) if needed, which is always nice.
I'd use a form class instead of nested attributes any day, see http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ for an example of this pattern (Chapter "3. Extract Form Objects")
I've used the pattern often enough to gemify it https://github.com/bbozo/simple_form_class and it's used roughly in this way: https://gist.github.com/bbozo/5036937, if you're interested to use it I'll push some docs
EDIT: reason why I tend to go the form class route most of the time is because nested attributes never failed to bite me in the end, either because strong parameter handling got cumbersome, or validators get too complicated (if persisted? else ...), or persistence logic needs to be extended to support some little knack that resolves into callback hell or recursive saves in the model (before/after save spaghetti troubles)
Using Mongoid 2.4.5 on Rails 3.2.1
I have a Model Book that has_many :pages.
class Book
include Mongoid::Document
has_many :pages
end
class Page
include Mongoid::Document
field :page_number
belongs_to :book
validates_uniqueness_of :page_number, scope: :book
end
I'm using nested resources so that I can get urls like /books/4f450e7a84b93e2b44000001/pages/4f4bba1384b93ea750000003/
What I would like to be able to do is use a url like /books/4f450e7a84b93e2b44000001/pages/3/ to get the third page in that book.
Now the crux of the question:
I want to find the page via a call like Book.find('4f450e7a84b93e2b44000001').pages.find('3') or like Book.find('4f450e7a84b93e2b44000001').pages.find('4f4bba1384b93ea750000003')
I know that I can override the find method in Page with something like
class << self
def find(*args)
where(:page_number => args.first).first || super(args)
end
end
But that doesn't seem to have any effect on the scoped query book.pages.find('3') as it seems the scoped search uses a different find method.
How do I specifically override the find method used by book.pages.find('3')?
Why just do a where criteria on your pages ?
Book.find('4f450e7a84b93e2b44000001').pages.where( :page_number => '3')
You can do a scope to in your Pages
class Page
scope :page_number, lambda{|num| where(:page_number => num) }
end
and use it like :
Book.find('4f450e7a84b93e2b44000001').pages.page_number('3')
Define a to_param method on your Page model that returns the page number. This way all Rails URL helpers use that when building URLs (automatically). Then you can just use something like
#book.pages.where(:page_number => params[:page_id]) # page_id is actually the result of page#to_param
Btw. I don't know how large your books are, but it might make more sense to embed your Pages in the Book from a document-oriented database point of view. The whole relationship business is not native to MongoDB.
By 'view' here I mean different combinations of properties of the model, not the view of the traditional MVC. For example, I have the following model:
class Game < ActiveRecord::Base
has_many :players
belongs_to :status
has_one :deck
has_many :turns
has_one :current_turn, :class_name => 'Turn', :conditions => ['turn_num = ?', '#{self.turn_num}']
end
I've written a full_xml method for Game that I use for the 'normal' get operation, so that I can include certain properties of players and current_turn, and then I don't have to do GETs on every player all the time. I also don't want to include ALL the properties and children and children's properties of the Game model on every GET
Now, however, I want to GET a game history, which is all the turns (and their properties/children). Initially I thought of a new model w/out a corresponding table, and then realized that wasn't necessary because the data and relationships are already there in the game and turns models. I also thought about writing a new action, but I thought I read somewhere that in the RESTful world, you shouldn't be writing any actions other than the core 7.
BTW, I'm thinking here of returning xml, because I'm using a Flex front end instead of rails views.
You have a couple of options here - I would use "nested resources" so you end up with a /game/:game_id/turns route which calls 'index' on the Turns controller. The other option is to create a GameHistory controller, which might be useful if there is additional logic associated with your game history.
There is not a one-to-one correspondence between controllers and models; there is however a one-to-one correspondence between controllers and RESOURCES. A game history is a whole different resource from a Game, just like a user session resource is different from an actual user resource (this is commonly used to allow for RESTful logins as well as RESTful user management) Hopefully this helps :)
Yes, nested resources was the answer. This Railscast explains it nicely. I had briefly tried nested resources before, and couldn't get it to work. It was returning all the child resources, not only the nested resource of the parent resource. This was because I assumed Rails was automagically doing that for me, which it doesn't. The Railscast explains that you still have to make changes to the controller of the child resources, like so:
class TurnsController < ApplicationController
# GET /turns
# GET /turns.xml
def index
#game = Game.find(params[:game_id])
#turns = #game.turns
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #turns.to_xml( :except => [:created_at, :updated_at] ) }
end
end
... more methods
end
You also have to edit your routes.rb file. In this case, I want nested routes for both players and turns of the game, so I did this:
map.resources :games do |game|
game.resources :players
game.resources :turns
end