Merge pull request #116 from AyuntamientoMadrid/admin-77

Administrator and Moderator basic interface
This commit is contained in:
Raimond Garcia
2015-08-10 16:41:47 +02:00
30 changed files with 373 additions and 31 deletions

View File

@@ -28,6 +28,7 @@ gem 'foundation-rails'
gem 'acts_as_votable' gem 'acts_as_votable'
gem "recaptcha", require: "recaptcha/rails" gem "recaptcha", require: "recaptcha/rails"
gem 'ckeditor' gem 'ckeditor'
gem 'cancancan'
group :development, :test do group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console # Call 'byebug' anywhere in the code to stop execution and get a debugger console

View File

@@ -53,6 +53,7 @@ GEM
builder (3.2.2) builder (3.2.2)
byebug (5.0.0) byebug (5.0.0)
columnize (= 0.9.0) columnize (= 0.9.0)
cancancan (1.12.0)
capistrano (3.4.0) capistrano (3.4.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@@ -290,6 +291,7 @@ DEPENDENCIES
acts_as_commentable_with_threading acts_as_commentable_with_threading
acts_as_votable acts_as_votable
byebug byebug
cancancan
capistrano (= 3.4.0) capistrano (= 3.4.0)
capistrano-bundler (= 1.1.4) capistrano-bundler (= 1.1.4)
capistrano-passenger capistrano-passenger

View File

@@ -2,6 +2,7 @@ class AccountController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_account before_action :set_account
load_and_authorize_resource class: "User"
def show def show
end end

View File

@@ -0,0 +1,13 @@
class Admin::BaseController < ApplicationController
before_action :authenticate_user!
skip_authorization_check
before_action :verify_administrator
private
def verify_administrator
raise CanCan::AccessDenied unless current_user.try(:administrator?)
end
end

View File

@@ -0,0 +1,6 @@
class Admin::DashboardController < Admin::BaseController
def index
end
end

View File

@@ -1,6 +1,9 @@
require "application_responder" require "application_responder"
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller?
self.responder = ApplicationResponder self.responder = ApplicationResponder
respond_to :html respond_to :html
@@ -11,6 +14,10 @@ class ApplicationController < ActionController::Base
# For APIs, you may want to use :null_session instead. # For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception protect_from_forgery with: :exception
rescue_from CanCan::AccessDenied do |exception|
redirect_to main_app.root_url, alert: exception.message
end
private private
def set_locale def set_locale

View File

@@ -1,12 +1,12 @@
class CommentsController < ApplicationController class CommentsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_debate, :set_parent, only: :create before_action :build_comment, only: :create
load_and_authorize_resource
respond_to :html, :js respond_to :html, :js
def create def create
@comment = Comment.build(@debate, current_user, params[:comment][:body])
@comment.save! @comment.save!
@comment.move_to_child_of(@parent) if reply? @comment.move_to_child_of(parent) if reply?
Mailer.comment(@comment).deliver_now if email_on_debate_comment? Mailer.comment(@comment).deliver_now if email_on_debate_comment?
Mailer.reply(@comment).deliver_now if email_on_comment_reply? Mailer.reply(@comment).deliver_now if email_on_comment_reply?
@@ -15,7 +15,6 @@ class CommentsController < ApplicationController
end end
def vote def vote
@comment = Comment.find(params[:id])
@comment.vote_by(voter: current_user, vote: params[:value]) @comment.vote_by(voter: current_user, vote: params[:value])
respond_with @comment respond_with @comment
end end
@@ -25,16 +24,20 @@ class CommentsController < ApplicationController
params.require(:comments).permit(:commentable_type, :commentable_id, :body) params.require(:comments).permit(:commentable_type, :commentable_id, :body)
end end
def set_debate def build_comment
@debate = Debate.find(params[:debate_id]) @comment = Comment.build(debate, current_user, params[:comment][:body])
end end
def set_parent def debate
@parent = Comment.find_parent(params[:comment]) @debate ||= Debate.find(params[:debate_id])
end
def parent
@parent ||= Comment.find_parent(params[:comment])
end end
def reply? def reply?
@parent.class == Comment parent.class == Comment
end end
def email_on_debate_comment? def email_on_debate_comment?
@@ -42,6 +45,6 @@ class CommentsController < ApplicationController
end end
def email_on_comment_reply? def email_on_comment_reply?
reply? && @parent.author.email_on_comment_reply? reply? && parent.author.email_on_comment_reply?
end end
end end

View File

@@ -1,8 +1,7 @@
class DebatesController < ApplicationController class DebatesController < ApplicationController
include RecaptchaHelper include RecaptchaHelper
before_action :set_debate, only: [:show, :edit, :update, :vote]
before_action :authenticate_user!, except: [:index, :show] before_action :authenticate_user!, except: [:index, :show]
before_action :validate_ownership, only: [:edit, :update] load_and_authorize_resource
def index def index
if params[:tag] if params[:tag]
@@ -56,10 +55,6 @@ class DebatesController < ApplicationController
params.require(:debate).permit(:title, :description, :tag_list, :terms_of_service) params.require(:debate).permit(:title, :description, :tag_list, :terms_of_service)
end end
def validate_ownership
raise ActiveRecord::RecordNotFound unless @debate.editable_by?(current_user)
end
def set_voted_values(debates_ids) def set_voted_values(debates_ids)
@voted_values = current_user ? current_user.votes_on_debates(debates_ids) : {} @voted_values = current_user ? current_user.votes_on_debates(debates_ids) : {}
end end

View File

@@ -0,0 +1,13 @@
class Moderation::BaseController < ApplicationController
before_action :authenticate_user!
skip_authorization_check
before_action :verify_moderator
private
def verify_moderator
raise CanCan::AccessDenied unless current_user.try(:moderator?) || current_user.try(:administrator?)
end
end

View File

@@ -0,0 +1,6 @@
class Moderation::DashboardController < Moderation::BaseController
def index
end
end

26
app/models/ability.rb Normal file
View File

@@ -0,0 +1,26 @@
class Ability
include CanCan::Ability
def initialize(user)
# Not logged in users
can :read, Debate
if user # logged-in users
can [:read, :update], User, id: user.id
can [:read, :create, :vote], Debate
can :update, Debate do |debate|
debate.editable_by?(user)
end
can [:create, :vote], Comment
if user.moderator? or user.administrator?
elsif user.administrator?
end
end
end
end

View File

@@ -0,0 +1,6 @@
class Administrator < ActiveRecord::Base
belongs_to :user
delegate :name, :email, to: :user
validates :user_id, presence: true, uniqueness: true
end

6
app/models/moderator.rb Normal file
View File

@@ -0,0 +1,6 @@
class Moderator < ActiveRecord::Base
belongs_to :user
delegate :name, :email, to: :user
validates :user_id, presence: true, uniqueness: true
end

View File

@@ -19,4 +19,12 @@ class User < ActiveRecord::Base
voted = votes.where("votable_type = ? AND votable_id IN (?)", "Debate", debates_ids) voted = votes.where("votable_type = ? AND votable_id IN (?)", "Debate", debates_ids)
voted.each_with_object({}){ |v,_| _[v.votable_id] = v.vote_flag } voted.each_with_object({}){ |v,_| _[v.votable_id] = v.vote_flag }
end end
def administrator?
@is_administrator ||= Administrator.where(user_id: id).exists?
end
def moderator?
@is_moderator ||= Moderator.where(user_id: id).exists?
end
end end

View File

@@ -0,0 +1 @@
<h1><%= t("admin.dashboard.index.title") %></h1>

View File

@@ -0,0 +1 @@
<h1><%= t("moderation.dashboard.index.title") %></h1>

View File

@@ -85,6 +85,8 @@ search:
# ignore_missing: # ignore_missing:
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}' # - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
# - '{devise,simple_form}.*' # - '{devise,simple_form}.*'
ignore_missing:
- 'unauthorized.*'
## Consider these keys used: ## Consider these keys used:
ignore_unused: ignore_unused:
@@ -93,6 +95,8 @@ ignore_unused:
# - 'simple_form.{yes,no}' # - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*' # - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:' # - 'simple_form.{error_notification,required}.:'
ignore_unused:
- 'unauthorized.*'
## Exclude these keys from the `i18n-tasks eq-base' report: ## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base: # ignore_eq_base:

View File

@@ -12,6 +12,14 @@ en:
create_debate: Create a debate create_debate: Create a debate
my_account_link: My account my_account_link: My account
language: Site language language: Site language
admin:
dashboard:
index:
title: Administration
moderation:
dashboard:
index:
title: Moderation
debates: debates:
index: index:
create_debate: Create a debate create_debate: Create a debate
@@ -81,3 +89,7 @@ en:
subject: Someone has commented on your debate subject: Someone has commented on your debate
reply: reply:
subject: Someone has replied to your comment subject: Someone has replied to your comment
unauthorized:
default: "You are not authorized to access this page."
manage:
all: "You are not authorized to %{action} %{subject}."

View File

@@ -12,6 +12,14 @@ es:
create_debate: Crea un debate create_debate: Crea un debate
my_account_link: Mi cuenta my_account_link: Mi cuenta
language: Idioma de la página language: Idioma de la página
admin:
dashboard:
index:
title: Administración
moderation:
dashboard:
index:
title: Moderación
debates: debates:
index: index:
create_debate: Crea un debate create_debate: Crea un debate
@@ -81,3 +89,21 @@ es:
subject: Alguien ha comentado en tu debate subject: Alguien ha comentado en tu debate
reply: reply:
subject: Alguien ha respondido a tu comentario subject: Alguien ha respondido a tu comentario
unauthorized:
default: "No tienes permiso para acceder a esta página."
index:
all: "No tienes permiso para listar %{subject}"
show:
all: "No tienes permiso para ver %{subject}"
edit:
all: "No tienes permiso para editar %{subject}"
update:
all: "No tienes permiso para modificar %{subject}"
create:
all: "No tienes permiso para crear %{subject}"
delete:
all: "No tienes permiso para borrar %{subject}"
manage:
all: "No tienes permiso para realizar la acción '%{action}' sobre %{subject}."

View File

@@ -17,11 +17,18 @@ Rails.application.routes.draw do
post :vote post :vote
end end
end end
end end
resource :account, controller: "account", only: [:show, :update] resource :account, controller: "account", only: [:show, :update]
namespace :admin do
root to: "dashboard#index"
end
namespace :moderation do
root to: "dashboard#index"
end
# Example of regular route: # Example of regular route:
# get 'products/:id' => 'catalog#view' # get 'products/:id' => 'catalog#view'

View File

@@ -0,0 +1,7 @@
class CreateAdministrators < ActiveRecord::Migration
def change
create_table :administrators do |t|
t.belongs_to :user, index: true, foreign_key: true
end
end
end

View File

@@ -0,0 +1,7 @@
class CreateModerators < ActiveRecord::Migration
def change
create_table :moderators do |t|
t.belongs_to :user, index: true, foreign_key: true
end
end
end

View File

@@ -11,11 +11,17 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150806163142) do ActiveRecord::Schema.define(version: 20150807140346) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
create_table "administrators", force: :cascade do |t|
t.integer "user_id"
end
add_index "administrators", ["user_id"], name: "index_administrators_on_user_id", using: :btree
create_table "comments", force: :cascade do |t| create_table "comments", force: :cascade do |t|
t.integer "commentable_id" t.integer "commentable_id"
t.string "commentable_type" t.string "commentable_type"
@@ -41,6 +47,12 @@ ActiveRecord::Schema.define(version: 20150806163142) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "moderators", force: :cascade do |t|
t.integer "user_id"
end
add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree
create_table "taggings", force: :cascade do |t| create_table "taggings", force: :cascade do |t|
t.integer "tag_id" t.integer "tag_id"
t.integer "taggable_id" t.integer "taggable_id"
@@ -105,4 +117,6 @@ ActiveRecord::Schema.define(version: 20150806163142) do
add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree
add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree
add_foreign_key "administrators", "users"
add_foreign_key "moderators", "users"
end end

View File

@@ -31,4 +31,12 @@ FactoryGirl.define do
debate debate
end end
factory :administrator do
user
end
factory :moderator do
user
end
end end

View File

@@ -10,6 +10,7 @@ feature 'Account' do
login_as(@user) login_as(@user)
visit root_path visit root_path
click_link "My account" click_link "My account"
expect(current_path).to eq(account_path)
expect(page).to have_selector("input[value='Manuela']") expect(page).to have_selector("input[value='Manuela']")
expect(page).to have_selector("input[value='Colau']") expect(page).to have_selector("input[value='Colau']")

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
feature 'Admin' do
let(:user) { create(:user) }
scenario 'Access as regular user is not authorized' do
login_as(user)
visit admin_root_path
expect(current_path).to eq(root_path)
expect(page).to have_content "not authorized"
end
scenario 'Access as a moderator is not authorized' do
create(:moderator, user: user)
login_as(user)
visit admin_root_path
expect(current_path).to eq(root_path)
expect(page).to have_content "not authorized"
end
scenario 'Access as an administrator is authorized' do
create(:administrator, user: user)
login_as(user)
visit admin_root_path
expect(current_path).to eq(admin_root_path)
expect(page).to_not have_content "not authorized"
end
end

View File

@@ -98,9 +98,9 @@ feature 'Debates' do
expect(debate).to be_editable expect(debate).to be_editable
login_as(create(:user)) login_as(create(:user))
expect { visit edit_debate_path(debate)
visit edit_debate_path(debate) expect(current_path).to eq(root_path)
}.to raise_error ActiveRecord::RecordNotFound expect(page).to have_content 'not authorized'
end end
scenario 'Update should not be posible if debate is not editable' do scenario 'Update should not be posible if debate is not editable' do
@@ -109,17 +109,19 @@ feature 'Debates' do
expect(debate).to_not be_editable expect(debate).to_not be_editable
login_as(debate.author) login_as(debate.author)
expect { visit edit_debate_path(debate)
visit edit_debate_path(debate) edit_debate_path(debate)
}.to raise_error ActiveRecord::RecordNotFound expect(current_path).to eq(root_path)
expect(page).to have_content 'not authorized'
end end
scenario 'Update should be posible for the author of an editable debate' do scenario 'Update should be posible for the author of an editable debate' do
debate = create(:debate) debate = create(:debate)
login_as(debate.author) login_as(debate.author)
visit debate_path(debate) visit edit_debate_path(debate)
click_link 'Edit' expect(current_path).to eq(edit_debate_path(debate))
fill_in 'debate_title', with: "End child poverty" fill_in 'debate_title', with: "End child poverty"
fill_in 'debate_description', with: "Let's..." fill_in 'debate_description', with: "Let's..."

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
feature 'Admin' do
let(:user) { create(:user) }
scenario 'Access as regular user is not authorized' do
login_as(user)
visit moderation_root_path
expect(current_path).to eq(root_path)
expect(page).to have_content "not authorized"
end
scenario 'Access as a moderator is authorized' do
create(:moderator, user: user)
login_as(user)
visit moderation_root_path
expect(current_path).to eq(moderation_root_path)
expect(page).to_not have_content "not authorized"
end
scenario 'Access as an administrator is authorized' do
create(:administrator, user: user)
login_as(user)
visit moderation_root_path
expect(current_path).to eq(moderation_root_path)
expect(page).to_not have_content "not authorized"
end
end

View File

@@ -0,0 +1,67 @@
require 'rails_helper'
require 'cancan/matchers'
describe Ability do
subject(:ability) { Ability.new(user) }
let(:debate) { Debate.new }
describe "Non-logged in user" do
let(:user) { nil }
it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) }
it { should_not be_able_to(:edit, Debate) }
it { should_not be_able_to(:vote, Debate) }
end
describe "Citizen" do
let(:user) { create(:user) }
it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) }
it { should be_able_to(:vote, debate) }
it { should be_able_to(:show, user) }
it { should be_able_to(:edit, user) }
it { should be_able_to(:create, Comment) }
it { should be_able_to(:vote, Comment) }
describe "other users" do
let(:other_user) { create(:user) }
it { should_not be_able_to(:show, other_user) }
it { should_not be_able_to(:edit, other_user) }
end
describe "editing debates" do
let(:own_debate) { create(:debate, author: user) }
let(:own_debate_non_editable) { create(:debate, author: user) }
before { allow(own_debate_non_editable).to receive(:editable?).and_return(false) }
it { should be_able_to(:edit, own_debate) }
it { should_not be_able_to(:edit, debate) } # Not his
it { should_not be_able_to(:edit, own_debate_non_editable) }
end
end
describe "Moderator" do
let(:user) { create(:user) }
before { create(:moderator, user: user) }
it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) }
it { should be_able_to(:vote, debate) }
end
describe "Administrator" do
let(:user) { create(:user) }
before { create(:administrator, user: user) }
it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) }
it { should be_able_to(:vote, debate) }
end
end

View File

@@ -7,13 +7,13 @@ describe User do
@user = create(:user) @user = create(:user)
end end
it "should return {} if no debate" do it "returns {} if no debate" do
expect(@user.votes_on_debates()).to eq({}) expect(@user.votes_on_debates()).to eq({})
expect(@user.votes_on_debates([])).to eq({}) expect(@user.votes_on_debates([])).to eq({})
expect(@user.votes_on_debates([nil, nil])).to eq({}) expect(@user.votes_on_debates([nil, nil])).to eq({})
end end
it "should return a hash of debates ids and votes" do it "returns a hash of debates ids and votes" do
debate1 = create(:debate) debate1 = create(:debate)
debate2 = create(:debate) debate2 = create(:debate)
debate3 = create(:debate) debate3 = create(:debate)
@@ -87,4 +87,28 @@ describe User do
end end
end end
describe "administrator?" do
it "is false when the user is not an admin" do
expect(subject.administrator?).to be false
end
it "is true when the user is an admin" do
subject.save
create(:administrator, user: subject)
expect(subject.administrator?).to be true
end
end
describe "moderator?" do
it "is false when the user is not a moderator" do
expect(subject.moderator?).to be false
end
it "is true when the user is a moderator" do
subject.save
create(:moderator, user: subject)
expect(subject.moderator?).to be true
end
end
end end