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 "recaptcha", require: "recaptcha/rails"
gem 'ckeditor'
gem 'cancancan'
group :development, :test do
# 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)
byebug (5.0.0)
columnize (= 0.9.0)
cancancan (1.12.0)
capistrano (3.4.0)
i18n
rake (>= 10.0.0)
@@ -290,6 +291,7 @@ DEPENDENCIES
acts_as_commentable_with_threading
acts_as_votable
byebug
cancancan
capistrano (= 3.4.0)
capistrano-bundler (= 1.1.4)
capistrano-passenger

View File

@@ -2,6 +2,7 @@ class AccountController < ApplicationController
before_action :authenticate_user!
before_action :set_account
load_and_authorize_resource class: "User"
def show
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"
class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller?
self.responder = ApplicationResponder
respond_to :html
@@ -11,6 +14,10 @@ class ApplicationController < ActionController::Base
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
rescue_from CanCan::AccessDenied do |exception|
redirect_to main_app.root_url, alert: exception.message
end
private
def set_locale

View File

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

View File

@@ -1,8 +1,7 @@
class DebatesController < ApplicationController
include RecaptchaHelper
before_action :set_debate, only: [:show, :edit, :update, :vote]
before_action :authenticate_user!, except: [:index, :show]
before_action :validate_ownership, only: [:edit, :update]
load_and_authorize_resource
def index
if params[:tag]
@@ -56,10 +55,6 @@ class DebatesController < ApplicationController
params.require(:debate).permit(:title, :description, :tag_list, :terms_of_service)
end
def validate_ownership
raise ActiveRecord::RecordNotFound unless @debate.editable_by?(current_user)
end
def set_voted_values(debates_ids)
@voted_values = current_user ? current_user.votes_on_debates(debates_ids) : {}
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.each_with_object({}){ |v,_| _[v.votable_id] = v.vote_flag }
end
def administrator?
@is_administrator ||= Administrator.where(user_id: id).exists?
end
def moderator?
@is_moderator ||= Moderator.where(user_id: id).exists?
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:
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
# - '{devise,simple_form}.*'
ignore_missing:
- 'unauthorized.*'
## Consider these keys used:
ignore_unused:
@@ -93,6 +95,8 @@ ignore_unused:
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
ignore_unused:
- 'unauthorized.*'
## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:

View File

@@ -12,6 +12,14 @@ en:
create_debate: Create a debate
my_account_link: My account
language: Site language
admin:
dashboard:
index:
title: Administration
moderation:
dashboard:
index:
title: Moderation
debates:
index:
create_debate: Create a debate
@@ -81,3 +89,7 @@ en:
subject: Someone has commented on your debate
reply:
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
my_account_link: Mi cuenta
language: Idioma de la página
admin:
dashboard:
index:
title: Administración
moderation:
dashboard:
index:
title: Moderación
debates:
index:
create_debate: Crea un debate
@@ -81,3 +89,21 @@ es:
subject: Alguien ha comentado en tu debate
reply:
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
end
end
end
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:
# 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.
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
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|
t.integer "commentable_id"
t.string "commentable_type"
@@ -41,6 +47,12 @@ ActiveRecord::Schema.define(version: 20150806163142) do
t.datetime "updated_at", null: false
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|
t.integer "tag_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", ["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

View File

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

View File

@@ -10,6 +10,7 @@ feature 'Account' do
login_as(@user)
visit root_path
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='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
login_as(create(:user))
expect {
visit edit_debate_path(debate)
}.to raise_error ActiveRecord::RecordNotFound
expect(current_path).to eq(root_path)
expect(page).to have_content 'not authorized'
end
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
login_as(debate.author)
expect {
visit edit_debate_path(debate)
}.to raise_error ActiveRecord::RecordNotFound
edit_debate_path(debate)
expect(current_path).to eq(root_path)
expect(page).to have_content 'not authorized'
end
scenario 'Update should be posible for the author of an editable debate' do
debate = create(:debate)
login_as(debate.author)
visit debate_path(debate)
click_link 'Edit'
visit edit_debate_path(debate)
expect(current_path).to eq(edit_debate_path(debate))
fill_in 'debate_title', with: "End child poverty"
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)
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([nil, nil])).to eq({})
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)
debate2 = create(:debate)
debate3 = create(:debate)
@@ -87,4 +87,28 @@ describe User do
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