I'm making an extremely simple CMS with rails that simply changes existing text and images on existing pages, nothing else. No changes to layout or anything crazy like that.
It's a bit like wordpress. Here's how it'll work:
If an admin is logged in, they have a special bar at the top of the
page that will enable them to edit the page.
Upon clicking, they go to the control panel, and all of the editable regions of viewable.
They can then edit the pages in the control panel, and save it.
What would be a good way to organize this flow?
I've made a start, and one thing that gets me is that all of the logic that populates a page' editable regions is occurring in the view:
splash/welcome.html.erb
<% #page = Page.find(name: 'welcome') %>
<% regions = #page.text_regions %>
<h1> <%= regions.where(name: 'title').text %> </h1> This returns the title (Welcome to my website)
<%= regions.where(name: 'welcometext').text %> This returns welcome text (This is my website bla bla)
I works fine, although the database has to be initially seeded with empty regions.
However, I don't like how there is logic in the view, so how could I populate a #regions instance variable in the application controller? Or is what I'm doing fine?
Maybe in the future I want users to be able to create pages, meaning I won't be able to have this logic in the view.
DRY
Looks like you'd be better using action_name (perhaps with a helper)
Without knowing the structure of what you're trying to do, this is just speculation, but you may be better doing something like this:
#app/models/region.rb
Class Region < ActiveRecord::Base
scope :title, -> { find_by name: "title" }
scope :welcome_text, -> { find_by name: "welcometext" }
end
#app/controllers/application_controller.rb
Class ApplicationController < ActionController::Base
before_action :set_page
private
def set_page
#page = Page.find_by name: action_name
#regions = #page.text_regions
end
end
#app/views/shared/_regions.html.erb
<h1><%= #regions.title.text %></h1>
<%= #regions.welcome_text %>
#app/views/static/welcome.html.erb
<%= render "shared/regions" %>
Hopefully this will give you some ideas as to how to achieve what you want?
Related
I've only ever used a static default scope for my projects and my latest one I wanted to create a dynamic scope feature, where the user can click a button or use a dropdown menu to change the order they view a list.
so far I've come across a few methods like using unscoping and setting a new scope, and seen reorder. I'm looking at the docs but I also am unsure of how to make a user be able to choose. Would I use something like Link_to, or button_to in the corresponding HTML.erb file?
in my post.rb file it'd look like this
default_scope { order(created_at: :desc) }
scope :ordered_by_title, -> { reorder(title: :asc) }
scope :ordered_by_reverse_created_at, -> { reorder(created_at: :ASC)}
I added those other scopes under the default because I assumed one would set the scopes they wanted and the view would call/activate them once a user clicks or chooses it from the rendered page.
in my post_controller.rb
def index
#posts = Post.all
end
in my index.html.erb the view
I have the list of posts rendered this way, if it won't work with what I want to do can someone show me a better way to do it?
# <some way for user to choose those defined scopes and render the new page would go here>
<% #posts.each do |post| %>
<div class="media">
<div class="media-body">
<h4 class="media-heading">
<%= link_to post.title, post %>
<small> <%= post.body %> </small>
</h4>
</div>
</div>
<% end %>
There are basically two ways to do this:
purely front end, use js. You can use some existing js plugin with sorting abilities if you don't feel like writing your own (e.g. DataTables). You'll probably need to make a table with the different attributes available for this to work.
back end, use ajax. Write your own or use something like filterrific, which uses scopes for filtering and sorting.
There doesn't appear to be a gem for this, and I think a CMS is overkill as the client only wants to edit the welcome message on the home page!
Here's what I think I should do:
1) Create Page model:
rails g model Page name:string
2) Create Field model:
rails g model Field name:string content:string page_id:integer
3) Create relationship, Page h1:b2 Field
4) Create rake task to set up the message field that belongs to the welcome page:
namespace :seeder do
namespace :initial_seed do
task pages: :environment do
p = Page.create(name: "Welcome")
p.fields.create(name: "welcomemessage", content: "everything goes here. The long rambling welcome!")
end
end
end
5) Create a 'static' controller for the 'static'-ish pages. The home, the about us etc...
class Static < ApplicationController
def home
#fields = Page.where().fields
end
end
6) In the view, populate the welcome message from the database (I'll create a helper for this):
<% field = #fields.find {|x| x[:name] == 'welcomemessage' } %>
<%= field.content %>
So that's the reading done. Now onto the creation, updation and deletion:
6) Create a control panel controller:
class Panel < ApplicationController
def pages
#pages = Page.all
end
end
7) Display fields in the view at panel/pages.html.erb: (I'll use partials here)
<% #pages.each do |page| %>
Title: <%= page.name %>
<% page.fields.each do |field|%>
Field: <%= field.name %>
<% form_for(field) do |f| %>
<% f.text_area :content%>
<% f.submit %>
<%= end %>
<% end %>
<% end %>
Now this is just a rough run down of what I want to do. There are a few problems I want to query, though.
Is this sort of how you would do this?
How should I configure my routes? What is a clever way of populating the #fields variable (see step 5) with the fields for the page we're viewing?
If I do have a panel/pages.html.erb view, should it simply display all of the editable fields in text areas? How should it update these areas? Multiple submit buttons inside multiple forms? What if someone wants to edit many fields at once and submit them all at once?
Where should these forms go? Should I create multiple RESTful actions all inside the Panel controller like this?:
class Panel < ApplicationController
# new and create not present as the pages have to be created manually
# Enabling the user to create their own pages with their own layouts is a bit insane
def pages
#pages = Page.all
end
def pages_update
end
def pages_destroy
end
end
Multiple restful routes in one controller doesn't strike me as organised, but it would make it easier to lock down the panel controller with a before_action hook to redirect if not admin...
Also, I'm nearing the end of a big job, and all I need to do is add the ability to edit one field on one page and them I'm done and I really don't want to have to figure out alchemy_cms or whatever. In future, yes, but, please, please, please someone give me some small pointers here.
I would strongly advise against building your own CMS. It's fraught with difficulties, and it seems like you're running up against some of those now. You should go and check out something like AlchemyCMS.
So I'm trying to think about how to route my site and I need a little help. I have a business who can .build (as in business creates) buildings (sorry for the repetition haha) in a has_many. Each property has many something else.
I would like it so even though there will be more than one building, each business should only be able to view their own buildings, so if someone tries to alter a url, it would redirect home.
I have
resources :buildings
so as it is set up, anyone could just type in
host/buildings/whatever
I would like to redirect with an error if the building ID does not belong to the current_business (devise) it will redirect to their home page. each building has a business_id
Would I have to break the RESTful for this?
Thank you!
Assuming you have user_id in builduing resource:
buildings_controller.rb
def index
#buildings = current_user.buildings
end
def show
#building = current_user.buildings.find(params[:id])
end
buildings/index.html.erb
<% #buildings.each do |building| %>
<%= building.whatever_atribute %>
<% end %>
buildings/show.html.erb
<%= #building.whatever_atribute %>
With the above code when user will go to /buildings he will see only his buildings, and if he'll go to buildings/3 he will see this building if he owns it, in other case he will see a not found error that you can customize it with a redirect or display a styled page.
I'm trying to hide parts of my views depending on the User role.
So let's say I want only admins to be able to destroy Products. Besides the code in the controller for preventing regular users from destroying records, I would do the following in the view:
<% if current_user.admin? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
The previous code works, but it's prone to errors of omission, which may cause regular users to see links to actions they are not allowed to execute.
Also, if I decide later on that a new role (e.g. "moderator") can delete Products, I would have to find the views that display a delete link and add the logic allowing moderators to see it.
And if there are many models that can be deleted only by admin users (e.g. Promotion, User) maitenance of all the ifs would be pretty challenging.
Is there a better way of doing it? Maybe using helpers, or something similar? I'm looking for something maybe like this:
<%= destroy_link 'Delete', product %> # Only admins can see it
<%= edit_link 'Edit', promotion %> # Again, only admins see this link
<%= show_link 'Show', comment %> # Everyone sees this one
I found these two questions that are similar to mine, but none of them answered my question:
Show and hide based on user role in rails
Ruby on Rails (3) hiding parts of the view
I strongly recommend pundit.
It allows you to create "policies" for each model. For your Product model you might have a ProductPolicy that looks something like this
class ProductPolicy < ApplicationPolicy
def delete?
user.admin?
end
end
In your view you can do something like this
<% if policy(#post).delete? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
If later on you want to add a moderator role, just modify the policy method
class ProductPolicy < ApplicationPolicy
def delete?
user.admin? || user.moderator?
end
end
So I kind of figured a way to move the IFs out of the view. First, I override the link_to helper in my application_helper.rb:
def link_to(text, path, options={})
super(text, path, options) unless options[:admin] and !current_user.admin?
end
Then on my views I use it as:
<%= link_to 'Edit Product', product, admin: true, ... %>
This prevents regular users from seeing admin links, but for other html tags with content inside, such as divs, tables etc., an if would still be needed.
CanCan is another gem that lets you define "Abilities" per user role.
In views you can use something like if can? :delete, #post to check if the
user may delete that specific post.
Using the CanCan and Role gems, what is still needed is a way to Check The Route and see if "current_user" has permissions to access that Route based on their role(s) - then show/hide based on that.
This saves the user clicking on things and getting told they cannot see it - or us having to write per-item "if" logic specifying what roles can see what list-items (which the customer will change periodically, as roles are changed/refined) around every single link in one's menu (consider a bootstrap menu with 50+ items nested in groups with html formatting, etc), which is insane.
If we must put if-logic around each menu-item, let's use the exact same logic for every item by checking the role/permissions we already defined in the Ability file.
But in our menu-list, we have route-helpers - not "controller/method" info, so how to test the user's ability to hit the controller-action specified for the "path" in each link?
To get the controller and method (action) of a path (my examples use the 'users_path' route-helper) ...
Rails.application.routes.recognize_path(app.users_path)
=> {:controller=>"users", :action=>"index"}
Get just the controller-name
Rails.application.routes.recognize_path(app.users_path)[:controller]
=> "users"
Ability uses the Model for its breakdown, so convert from controller name to it's model (assuming default naming used) ...
Rails.application.routes.recognize_path(app.users_path)[:controller].classify
=> "User"
Get just the action-name
Rails.application.routes.recognize_path(app.users_path)[:action]
=> "index"
And since the "can?" method needs a Symbol for the action, and Constant for the model, for each menu-item we get this:
path_hash = Rails.application.routes.recognize_path(app.users_path)
model = path_hash[:controller].classify.constantize
action = path_hash[:action].to_sym
Then use our existing Abilty system to check if the current_user can access it, we have to pass the action as a symbol and the Model as a constant, so ...
<% if can? action model %>
<%= link_to "Users List", users_path %>
<% end %>
Now we can change who can see this resource and link from the Ability file, without ever messing with the menu, again. But to make this a bit cleaner, I extracted out the lookup for each menu-item with this in the app-controller:
def get_path_parts(path)
path_hash = Rails.application.routes.recognize_path(path)
model_name = path_hash[:controller].classify.constantize
action_name = path_hash[:action].to_sym
return [model_name, action_name]
end
helper_method :get_path_parts
... so I could do this in the view (I took out all the html-formatting from the links for simplicity, here):
<% path_parts = get_path_parts(users_path); if can?(path_parts[1], path_parts[0]) %>
<%= link_to "Users Listing", users_path %>
<% end %>
... and to make this not take all day typing these per-menu-item if-wraps, I used regex find/replace with capture and wildcards to wrap this around every list-item in the menu-item listing in one pass.
It's far from ideal, and I could do a lot more to make it much better, but I don't have spare-time to write the rest of this missing-piece of the Role/CanCan system. I hope this part helps someone out.
I've had a problem with the following issue in Rails and ASP.Net MVC. Often there are multiple widgets of functionality on a page, yet one controller action is supposed to render the page. Let me illustrate:
Let's say I have a normal e-commerce site, and the menu is made of categories, while the page is to display an group of products.
For the products, let's say I have an action on a controller that looks something like:
def product_list
#products = Products.find_by_category(:name => 'lawnmowers')
end
And I have a layout with something like
<div id="menu"><%= render :partial => 'menu' %></div>
<div id="content"><%= yield %></div>
The products have a view...
<%= render :partial => 'product', :collection => #products %>
(note I've ommited the product view as irrelevant)
And the menu has a partial...
<% Category.each {|c| %>
<%= render :partial => 'menu_node', :locals => { :category => c } %>
<% } %>
The line I have a problem with is the "Category.each.do" in the view. I'm fetching data in the view, as opposed to using variables that were set and bound in the controller. And it could easily be a more complex method call that produces the menu.
The solutions I've considered are:
-A view model base class that knows how to get various pieces of data. But you could end up with one of these for each conceptual "section" of the site.
-a local variable that populates at the top of each method (violates DRY)
-the same thing, but in a before_filter call
None of these seem very elegant to me. I can't help but look at this problem and think that a MVP presenter per view (not screen) is a more elegant solution.
ASP.Net MVC has render action (different from rails render :action), which does address this, but I'm not sure what I think of that solution.
Thoughts? Solution suggestions?
Added Note:
The answers provided so far are good suggestions. And they apply to the example I gave, where a menu is likely present in every layout, and is clearly secondary to the product data.
However, what if there is clearly no second class citizen? Portal type sites commonly have multiple unrelated widgets, in which each is important.
For example, What if this page was displaying weather trends, with widgets for temperature, humidity, and precipitation (and each is a different model and view type).
In rails we like to have a concept of thin-controllers, thick-models. So I think you're right to not want to have variables set in the controller.
Also, in order to enable a more-complex method later on, I recommend doing something like:
/app/controllers/application_controller.rb
before_filter :add_menu_nodes
def add_menu_nodes
#menu_nodes = Category.menu_nodes(current_user)
end
/app/views/layouts/application.html.erb
<%= render :partial=>:menu, :locals=>{:categories=>#menu_nodes} %>
/app/models/category.rb
def self.menu_nodes(current_user)
Category.all.order(:name)
end
That way in the future you could update Category.menu_nodes with a more complicated solution, based on the current user, if you need.
Forgive me if I butcher the Ruby (or misunderstand your question), but what's wrong with
class section_helper
def menu( section )
// ...
menuBuiltAbove
end
end
in the view
<%= section_helper.menu( 'section' ) %>
?