Commit dc737fd3 authored by jplang's avatar jplang

Merged 0.6 branch into trunk.

Permissions management was rewritten. Some permissions can now be specifically defined for non member and anonymous users.
This migration:
* is irreversible (please, don't forget to *backup* your database before upgrading)
* resets role's permissions (go to "Admin -> Roles & Permissions" to set them after upgrading)

git-svn-id: https://svn.redmine.org/redmine/trunk@674 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent f19fda16
......@@ -22,7 +22,6 @@ class AccountController < ApplicationController
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register]
before_filter :require_login, :only => :logout
# Show user's account
def show
......@@ -31,7 +30,7 @@ class AccountController < ApplicationController
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (logged_in_user && logged_in_user.role_for_project(membership.project))
membership.project.is_public? || (User.current.role_for_project(membership.project))
end
rescue ActiveRecord::RecordNotFound
render_404
......@@ -41,12 +40,12 @@ class AccountController < ApplicationController
def login
if request.get?
# Logout user
self.logged_in_user = nil
self.logged_user = nil
else
# Authenticate user
user = User.try_to_login(params[:login], params[:password])
if user
self.logged_in_user = user
self.logged_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
token = Token.create(:user => user, :action => 'autologin')
......@@ -62,8 +61,8 @@ class AccountController < ApplicationController
# Log out current user and redirect to welcome page
def logout
cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", logged_in_user.id, "autologin"]) if logged_in_user
self.logged_in_user = nil
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
self.logged_user = nil
redirect_to :controller => 'welcome'
end
......@@ -140,4 +139,15 @@ class AccountController < ApplicationController
end
end
end
private
def logged_user=(user)
if user && user.is_a?(User)
User.current = user
session[:user_id] = user.id
else
User.current = User.anonymous
session[:user_id] = nil
end
end
end
......@@ -46,14 +46,6 @@ class AdminController < ApplicationController
end
def mail_options
@actions = Permission.find(:all, :conditions => ["mail_option=?", true]) || []
if request.post?
@actions.each { |a|
a.mail_enabled = (params[:action_ids] || []).include? a.id.to_s
a.save
}
flash.now[:notice] = l(:notice_successful_update)
end
end
def test_email
......@@ -61,8 +53,8 @@ class AdminController < ApplicationController
# Force ActionMailer to raise delivery errors so we can catch it
ActionMailer::Base.raise_delivery_errors = true
begin
@test = Mailer.deliver_test(logged_in_user)
flash[:notice] = l(:notice_email_sent, logged_in_user.mail)
@test = Mailer.deliver_test(User.current)
flash[:notice] = l(:notice_email_sent, User.current.mail)
rescue Exception => e
flash[:error] = l(:notice_email_error, e.message)
end
......
......@@ -16,48 +16,47 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ApplicationController < ActionController::Base
before_filter :check_if_login_required, :set_localization
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def logged_in_user=(user)
@logged_in_user = user
session[:user_id] = (user ? user.id : nil)
def logged_in_user
User.current.logged? ? User.current : nil
end
def logged_in_user
def current_role
@current_role ||= User.current.role_for_project(@project)
end
def user_setup
if session[:user_id]
@logged_in_user ||= User.find(session[:user_id])
# existing session
User.current = User.find(session[:user_id])
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.current = User.find_by_autologin_key(autologin_key)
elsif params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication
User.current = User.find_by_rss_key(params[:key])
else
nil
User.current = User.anonymous
end
end
# Returns the role that the logged in user has on the current project
# or nil if current user is not a member of the project
def logged_in_user_membership
@user_membership ||= logged_in_user.role_for_project(@project)
end
# check if login is globally required to access the application
def check_if_login_required
# no check needed if user is already logged in
return true if logged_in_user
# auto-login feature
autologin_key = cookies[:autologin]
if autologin_key && Setting.autologin?
self.logged_in_user = User.find_by_autologin_key(autologin_key)
end
return true if User.current.logged?
require_login if Setting.login_required?
end
def set_localization
lang = begin
if self.logged_in_user and self.logged_in_user.language and !self.logged_in_user.language.empty? and GLoc.valid_languages.include? self.logged_in_user.language.to_sym
self.logged_in_user.language
if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
User.current.language
elsif request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
......@@ -71,7 +70,7 @@ class ApplicationController < ActionController::Base
end
def require_login
unless self.logged_in_user
if !User.current.logged?
store_location
redirect_to :controller => "account", :action => "login"
return false
......@@ -81,34 +80,17 @@ class ApplicationController < ActionController::Base
def require_admin
return unless require_login
unless self.logged_in_user.admin?
if !User.current.admin?
render_403
return false
end
true
end
# authorizes the user for the requested action.
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action])
unless @project.active?
@project = nil
render_404
return false
end
# check if action is allowed on public projects
if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ ctrl, action ]
return true
end
# if action is not public, force login
return unless require_login
# admin is always authorized
return true if self.logged_in_user.admin?
# if not admin, check membership permission
if logged_in_user_membership and Permission.allowed_to_role( "%s/%s" % [ ctrl, action ], logged_in_user_membership )
return true
end
render_403
false
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : (User.current.logged? ? render_403 : require_login)
end
# make sure that the user is a member of the project (or admin) if project is private
......@@ -119,11 +101,8 @@ class ApplicationController < ActionController::Base
render_404
return false
end
return true if @project.is_public?
return false unless logged_in_user
return true if logged_in_user.admin? || logged_in_user_membership
render_403
false
return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
User.current.logged? ? render_403 : require_login
end
# store current uri in session.
......@@ -154,6 +133,21 @@ class ApplicationController < ActionController::Base
render :template => "common/404", :layout => true, :status => 404
return false
end
def render_feed(items, options={})
@items = items.sort {|x,y| y.event_datetime <=> x.event_datetime }
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
def self.accept_key_auth(*actions)
actions = actions.flatten.map(&:to_s)
write_inheritable_attribute('accept_key_auth_actions', actions)
end
def accept_key_auth_actions
self.class.read_inheritable_attribute('accept_key_auth_actions') || []
end
# qvalues http header parser
# code taken from webrick
......@@ -173,4 +167,4 @@ class ApplicationController < ActionController::Base
end
return tmp
end
end
\ No newline at end of file
end
......@@ -17,9 +17,7 @@
class BoardsController < ApplicationController
layout 'base'
before_filter :find_project
before_filter :authorize, :except => [:index, :show]
before_filter :check_project_privacy, :only => [:index, :show]
before_filter :find_project, :authorize
helper :messages
include MessagesHelper
......
......@@ -52,7 +52,7 @@ class DocumentsController < ApplicationController
a = Attachment.create(:container => @document, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? #and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @document
end
......
......@@ -72,8 +72,15 @@ class IssuesController < ApplicationController
unless params[:notes].empty?
journal = @issue.init_journal(self.logged_in_user, params[:notes])
if @issue.save
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:value => a.filename) unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
Mailer.deliver_issue_edit(journal) #if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
return
end
......@@ -100,14 +107,14 @@ class IssuesController < ApplicationController
} if params[:attachments] and params[:attachments].is_a? Array
# Log time
if logged_in_user.authorized_to(@project, "timelog/edit")
if current_role.allowed_to?(:log_time)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
@time_entry.save
end
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
Mailer.deliver_issue_edit(journal) #if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
end
rescue ActiveRecord::StaleObjectError
......@@ -124,23 +131,6 @@ class IssuesController < ApplicationController
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project
end
def add_attachment
# Save the attachments
@attachments = []
journal = @issue.init_journal(self.logged_in_user)
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:value => a.filename) unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
journal.save if journal.details.any?
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
end
def destroy_attachment
a = @issue.attachments.find(params[:attachment_id])
a.destroy
......
......@@ -17,8 +17,7 @@
class MessagesController < ApplicationController
layout 'base'
before_filter :find_project, :check_project_privacy
before_filter :require_login, :only => [:new, :reply]
before_filter :find_project, :authorize
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
......
......@@ -22,6 +22,7 @@ class ProjectsController < ApplicationController
before_filter :find_project, :except => [ :index, :list, :add ]
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity, :calendar
cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
cache_sweeper :issue_sweeper, :only => [ :add_issue ]
......@@ -97,8 +98,7 @@ class ProjectsController < ApplicationController
@trackers = Tracker.find(:all, :order => 'position')
@open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
@total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
@key = logged_in_user.get_or_create_rss_key.value if logged_in_user
@key = User.current.rss_key
end
def settings
......@@ -224,7 +224,7 @@ class ProjectsController < ApplicationController
Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0
} if params[:attachments] and params[:attachments].is_a? Array
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_add(@document) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
Mailer.deliver_document_add(@document) #if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'list_documents', :id => @project
end
end
......@@ -268,7 +268,7 @@ class ProjectsController < ApplicationController
if @issue.save
@attachments.each(&:save)
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
Mailer.deliver_issue_add(@issue) #if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'list_issues', :id => @project
end
end
......@@ -383,7 +383,7 @@ class ProjectsController < ApplicationController
redirect_to :action => 'list_issues', :id => @project and return unless @issues
@projects = []
# find projects to which the user is allowed to move the issue
@logged_in_user.memberships.each {|m| @projects << m.project if Permission.allowed_to_role("projects/move_issues", m.role)}
User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:controller => 'projects', :action => 'move_issues')}
# issue can be moved to any tracker
@trackers = Tracker.find(:all)
if request.post? and params[:new_project_id] and params[:new_tracker_id]
......@@ -424,7 +424,11 @@ class ProjectsController < ApplicationController
# Show news list of @project
def list_news
@news_pages, @news = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "#{News.table_name}.created_on DESC"
render :action => "list_news", :layout => false if request.xhr?
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@news, :title => "#{@project.name}: #{l(:label_news_plural)}") }
end
end
def add_file
......@@ -437,7 +441,7 @@ class ProjectsController < ApplicationController
a = Attachment.create(:container => @version, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? #and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
@versions = @project.versions.sort
......@@ -471,80 +475,67 @@ class ProjectsController < ApplicationController
@year ||= Date.today.year
@month ||= Date.today.month
@date_from = Date.civil(@year, @month, 1)
@date_to = @date_from >> 1
case params[:format]
when 'rss'
# 30 last days
@date_from = Date.today - 30
@date_to = Date.today + 1
else
# current month
@date_from = Date.civil(@year, @month, 1)
@date_to = @date_from >> 1
end
@events_by_day = Hash.new { |h,k| h[k] = [] }
@event_types = %w(issues news attachments documents wiki_edits revisions)
@event_types.delete('wiki_edits') unless @project.wiki
@event_types.delete('changesets') unless @project.repository
unless params[:show_issues] == "0"
@project.issues.find(:all, :include => [:author], :conditions => ["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] << i
}
@project.issue_changes.find(:all, :include => :details, :conditions => ["(#{Journal.table_name}.created_on BETWEEN ? AND ?) AND (#{JournalDetail.table_name}.prop_key = 'status_id')", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] << i
}
@show_issues = 1
@scope = @event_types.select {|t| params["show_#{t}"]}
# default events if none is specified in parameters
@scope = (@event_types - %w(wiki_edits))if @scope.empty?
@events = []
if @scope.include?('issues')
@events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
end
unless params[:show_news] == "0"
@project.news.find(:all, :conditions => ["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to], :include => :author ).each { |i|
@events_by_day[i.created_on.to_date] << i
}
@show_news = 1
if @scope.include?('news')
@events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
end
unless params[:show_files] == "0"
Attachment.find(:all, :select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id",
:conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on BETWEEN ? AND ? ", @project.id, @date_from, @date_to],
:include => :author ).each { |i|
@events_by_day[i.created_on.to_date] << i
}
@show_files = 1
if @scope.include?('attachments')
@events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
end
unless params[:show_documents] == "0"
@project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] << i
}
Attachment.find(:all, :select => "attachments.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id",
:conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on BETWEEN ? AND ? ", @project.id, @date_from, @date_to],
:include => :author ).each { |i|
@events_by_day[i.created_on.to_date] << i
}
@show_documents = 1
if @scope.include?('documents')
@events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
@events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
end
unless @project.wiki.nil? || params[:show_wiki_edits] == "0"
if @scope.include?('wiki_edits') && @project.wiki
select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title"
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
"#{WikiContent.versioned_table_name}.id"
joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
@project.id, @date_from, @date_to]
WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions).each { |i|
# We provide this alias so all events can be treated in the same manner
def i.created_on
self.updated_on
end
@events_by_day[i.created_on.to_date] << i
}
@show_wiki_edits = 1
@events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
end
unless @project.repository.nil? || params[:show_changesets] == "0"
@project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]).each { |i|
def i.created_on
self.committed_on
end
@events_by_day[i.created_on.to_date] << i
}
@show_changesets = 1
if @scope.include?('revisions') && @project.repository
@events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
end
render :layout => false if request.xhr?
@events_by_day = @events.group_by(&:event_date)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
end
end
def calendar
......@@ -630,7 +621,7 @@ class ProjectsController < ApplicationController
def feeds
@queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
@key = logged_in_user.get_or_create_rss_key.value if logged_in_user
@key = User.current.rss_key
end
private
......
......@@ -16,9 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueriesController < ApplicationController
layout 'base'
before_filter :require_login, :except => :index
before_filter :find_project, :check_project_privacy
layout 'base'
before_filter :find_project, :authorize
def index
@queries = @project.queries.find(:all,
......@@ -31,7 +30,7 @@ class QueriesController < ApplicationController
@query.project = @project
@query.user = logged_in_user
@query.executed_by = logged_in_user
@query.is_public = false unless logged_in_user.authorized_to(@project, 'projects/add_query')
@query.is_public = false unless current_role.allowed_to?(:manage_pulic_queries)
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
......@@ -52,7 +51,7 @@ class QueriesController < ApplicationController
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.attributes = params[:query]
@query.is_public = false unless logged_in_user.authorized_to(@project, 'projects/add_query')
@query.is_public = false unless current_role.allowed_to?(:manage_pulic_queries)
if @query.save
flash[:notice] = l(:notice_successful_update)
......
......@@ -22,8 +22,8 @@ require 'digest/sha1'
class RepositoriesController < ApplicationController
layout 'base'
before_filter :find_project, :except => [:update_form]
before_filter :authorize, :except => [:update_form, :stats, :graph]
before_filter :check_project_privacy, :only => [:stats, :graph]
before_filter :authorize, :except => [:update_form]
accept_key_auth :revisions
def show
# check if new revisions have been committed in the repository
......@@ -57,7 +57,10 @@ class RepositoriesController < ApplicationController
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.</