Revision 1297:0a574315af3e .svn/pristine/20
| .svn/pristine/20/2006d03f57217f185b674373ebf261dbb0c3c839.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
require File.expand_path('../../../test_helper', __FILE__)
|
|
| 19 |
|
|
| 20 |
class ApiTest::MembershipsTest < ActionController::IntegrationTest |
|
| 21 |
fixtures :projects, :users, :roles, :members, :member_roles |
|
| 22 |
|
|
| 23 |
def setup |
|
| 24 |
Setting.rest_api_enabled = '1' |
|
| 25 |
end |
|
| 26 |
|
|
| 27 |
context "/projects/:project_id/memberships" do |
|
| 28 |
context "GET" do |
|
| 29 |
context "xml" do |
|
| 30 |
should "return memberships" do |
|
| 31 |
get '/projects/1/memberships.xml', {}, credentials('jsmith')
|
|
| 32 |
|
|
| 33 |
assert_response :success |
|
| 34 |
assert_equal 'application/xml', @response.content_type |
|
| 35 |
assert_tag :tag => 'memberships', |
|
| 36 |
:attributes => {:type => 'array'},
|
|
| 37 |
:child => {
|
|
| 38 |
:tag => 'membership', |
|
| 39 |
:child => {
|
|
| 40 |
:tag => 'id', |
|
| 41 |
:content => '2', |
|
| 42 |
:sibling => {
|
|
| 43 |
:tag => 'user', |
|
| 44 |
:attributes => {:id => '3', :name => 'Dave Lopper'},
|
|
| 45 |
:sibling => {
|
|
| 46 |
:tag => 'roles', |
|
| 47 |
:child => {
|
|
| 48 |
:tag => 'role', |
|
| 49 |
:attributes => {:id => '2', :name => 'Developer'}
|
|
| 50 |
} |
|
| 51 |
} |
|
| 52 |
} |
|
| 53 |
} |
|
| 54 |
} |
|
| 55 |
end |
|
| 56 |
end |
|
| 57 |
|
|
| 58 |
context "json" do |
|
| 59 |
should "return memberships" do |
|
| 60 |
get '/projects/1/memberships.json', {}, credentials('jsmith')
|
|
| 61 |
|
|
| 62 |
assert_response :success |
|
| 63 |
assert_equal 'application/json', @response.content_type |
|
| 64 |
json = ActiveSupport::JSON.decode(response.body) |
|
| 65 |
assert_equal({
|
|
| 66 |
"memberships" => |
|
| 67 |
[{"id"=>1,
|
|
| 68 |
"project" => {"name"=>"eCookbook", "id"=>1},
|
|
| 69 |
"roles" => [{"name"=>"Manager", "id"=>1}],
|
|
| 70 |
"user" => {"name"=>"John Smith", "id"=>2}},
|
|
| 71 |
{"id"=>2,
|
|
| 72 |
"project" => {"name"=>"eCookbook", "id"=>1},
|
|
| 73 |
"roles" => [{"name"=>"Developer", "id"=>2}],
|
|
| 74 |
"user" => {"name"=>"Dave Lopper", "id"=>3}}],
|
|
| 75 |
"limit" => 25, |
|
| 76 |
"total_count" => 2, |
|
| 77 |
"offset" => 0}, |
|
| 78 |
json) |
|
| 79 |
end |
|
| 80 |
end |
|
| 81 |
end |
|
| 82 |
|
|
| 83 |
context "POST" do |
|
| 84 |
context "xml" do |
|
| 85 |
should "create membership" do |
|
| 86 |
assert_difference 'Member.count' do |
|
| 87 |
post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith')
|
|
| 88 |
|
|
| 89 |
assert_response :created |
|
| 90 |
end |
|
| 91 |
end |
|
| 92 |
|
|
| 93 |
should "return errors on failure" do |
|
| 94 |
assert_no_difference 'Member.count' do |
|
| 95 |
post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith')
|
|
| 96 |
|
|
| 97 |
assert_response :unprocessable_entity |
|
| 98 |
assert_equal 'application/xml', @response.content_type |
|
| 99 |
assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"}
|
|
| 100 |
end |
|
| 101 |
end |
|
| 102 |
end |
|
| 103 |
end |
|
| 104 |
end |
|
| 105 |
|
|
| 106 |
context "/memberships/:id" do |
|
| 107 |
context "GET" do |
|
| 108 |
context "xml" do |
|
| 109 |
should "return the membership" do |
|
| 110 |
get '/memberships/2.xml', {}, credentials('jsmith')
|
|
| 111 |
|
|
| 112 |
assert_response :success |
|
| 113 |
assert_equal 'application/xml', @response.content_type |
|
| 114 |
assert_tag :tag => 'membership', |
|
| 115 |
:child => {
|
|
| 116 |
:tag => 'id', |
|
| 117 |
:content => '2', |
|
| 118 |
:sibling => {
|
|
| 119 |
:tag => 'user', |
|
| 120 |
:attributes => {:id => '3', :name => 'Dave Lopper'},
|
|
| 121 |
:sibling => {
|
|
| 122 |
:tag => 'roles', |
|
| 123 |
:child => {
|
|
| 124 |
:tag => 'role', |
|
| 125 |
:attributes => {:id => '2', :name => 'Developer'}
|
|
| 126 |
} |
|
| 127 |
} |
|
| 128 |
} |
|
| 129 |
} |
|
| 130 |
end |
|
| 131 |
end |
|
| 132 |
|
|
| 133 |
context "json" do |
|
| 134 |
should "return the membership" do |
|
| 135 |
get '/memberships/2.json', {}, credentials('jsmith')
|
|
| 136 |
|
|
| 137 |
assert_response :success |
|
| 138 |
assert_equal 'application/json', @response.content_type |
|
| 139 |
json = ActiveSupport::JSON.decode(response.body) |
|
| 140 |
assert_equal( |
|
| 141 |
{"membership" => {
|
|
| 142 |
"id" => 2, |
|
| 143 |
"project" => {"name"=>"eCookbook", "id"=>1},
|
|
| 144 |
"roles" => [{"name"=>"Developer", "id"=>2}],
|
|
| 145 |
"user" => {"name"=>"Dave Lopper", "id"=>3}}
|
|
| 146 |
}, |
|
| 147 |
json) |
|
| 148 |
end |
|
| 149 |
end |
|
| 150 |
end |
|
| 151 |
|
|
| 152 |
context "PUT" do |
|
| 153 |
context "xml" do |
|
| 154 |
should "update membership" do |
|
| 155 |
assert_not_equal [1,2], Member.find(2).role_ids.sort |
|
| 156 |
assert_no_difference 'Member.count' do |
|
| 157 |
put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith')
|
|
| 158 |
|
|
| 159 |
assert_response :ok |
|
| 160 |
assert_equal '', @response.body |
|
| 161 |
end |
|
| 162 |
member = Member.find(2) |
|
| 163 |
assert_equal [1,2], member.role_ids.sort |
|
| 164 |
end |
|
| 165 |
|
|
| 166 |
should "return errors on failure" do |
|
| 167 |
put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith')
|
|
| 168 |
|
|
| 169 |
assert_response :unprocessable_entity |
|
| 170 |
assert_equal 'application/xml', @response.content_type |
|
| 171 |
assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/}
|
|
| 172 |
end |
|
| 173 |
end |
|
| 174 |
end |
|
| 175 |
|
|
| 176 |
context "DELETE" do |
|
| 177 |
context "xml" do |
|
| 178 |
should "destroy membership" do |
|
| 179 |
assert_difference 'Member.count', -1 do |
|
| 180 |
delete '/memberships/2.xml', {}, credentials('jsmith')
|
|
| 181 |
|
|
| 182 |
assert_response :ok |
|
| 183 |
assert_equal '', @response.body |
|
| 184 |
end |
|
| 185 |
assert_nil Member.find_by_id(2) |
|
| 186 |
end |
|
| 187 |
|
|
| 188 |
should "respond with 422 on failure" do |
|
| 189 |
assert_no_difference 'Member.count' do |
|
| 190 |
# A membership with an inherited role can't be deleted |
|
| 191 |
Member.find(2).member_roles.first.update_attribute :inherited_from, 99 |
|
| 192 |
delete '/memberships/2.xml', {}, credentials('jsmith')
|
|
| 193 |
|
|
| 194 |
assert_response :unprocessable_entity |
|
| 195 |
end |
|
| 196 |
end |
|
| 197 |
end |
|
| 198 |
end |
|
| 199 |
end |
|
| 200 |
end |
|
| .svn/pristine/20/205eb718e22b02ea59cca1358f48bb87e5bd7f8d.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
require 'iconv' |
|
| 19 |
|
|
| 20 |
class Changeset < ActiveRecord::Base |
|
| 21 |
belongs_to :repository |
|
| 22 |
belongs_to :user |
|
| 23 |
has_many :filechanges, :class_name => 'Change', :dependent => :delete_all |
|
| 24 |
has_and_belongs_to_many :issues |
|
| 25 |
has_and_belongs_to_many :parents, |
|
| 26 |
:class_name => "Changeset", |
|
| 27 |
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
|
|
| 28 |
:association_foreign_key => 'parent_id', :foreign_key => 'changeset_id' |
|
| 29 |
has_and_belongs_to_many :children, |
|
| 30 |
:class_name => "Changeset", |
|
| 31 |
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
|
|
| 32 |
:association_foreign_key => 'changeset_id', :foreign_key => 'parent_id' |
|
| 33 |
|
|
| 34 |
acts_as_event :title => Proc.new {|o| o.title},
|
|
| 35 |
:description => :long_comments, |
|
| 36 |
:datetime => :committed_on, |
|
| 37 |
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
|
|
| 38 |
|
|
| 39 |
acts_as_searchable :columns => 'comments', |
|
| 40 |
:include => {:repository => :project},
|
|
| 41 |
:project_key => "#{Repository.table_name}.project_id",
|
|
| 42 |
:date_column => 'committed_on' |
|
| 43 |
|
|
| 44 |
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
|
|
| 45 |
:author_key => :user_id, |
|
| 46 |
:find_options => {:include => [:user, {:repository => :project}]}
|
|
| 47 |
|
|
| 48 |
validates_presence_of :repository_id, :revision, :committed_on, :commit_date |
|
| 49 |
validates_uniqueness_of :revision, :scope => :repository_id |
|
| 50 |
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true |
|
| 51 |
|
|
| 52 |
scope :visible, |
|
| 53 |
lambda {|*args| { :include => {:repository => :project},
|
|
| 54 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } |
|
| 55 |
|
|
| 56 |
after_create :scan_for_issues |
|
| 57 |
before_create :before_create_cs |
|
| 58 |
|
|
| 59 |
def revision=(r) |
|
| 60 |
write_attribute :revision, (r.nil? ? nil : r.to_s) |
|
| 61 |
end |
|
| 62 |
|
|
| 63 |
# Returns the identifier of this changeset; depending on repository backends |
|
| 64 |
def identifier |
|
| 65 |
if repository.class.respond_to? :changeset_identifier |
|
| 66 |
repository.class.changeset_identifier self |
|
| 67 |
else |
|
| 68 |
revision.to_s |
|
| 69 |
end |
|
| 70 |
end |
|
| 71 |
|
|
| 72 |
def committed_on=(date) |
|
| 73 |
self.commit_date = date |
|
| 74 |
super |
|
| 75 |
end |
|
| 76 |
|
|
| 77 |
# Returns the readable identifier |
|
| 78 |
def format_identifier |
|
| 79 |
if repository.class.respond_to? :format_changeset_identifier |
|
| 80 |
repository.class.format_changeset_identifier self |
|
| 81 |
else |
|
| 82 |
identifier |
|
| 83 |
end |
|
| 84 |
end |
|
| 85 |
|
|
| 86 |
def project |
|
| 87 |
repository.project |
|
| 88 |
end |
|
| 89 |
|
|
| 90 |
def author |
|
| 91 |
user || committer.to_s.split('<').first
|
|
| 92 |
end |
|
| 93 |
|
|
| 94 |
def before_create_cs |
|
| 95 |
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) |
|
| 96 |
self.comments = self.class.normalize_comments( |
|
| 97 |
self.comments, repository.repo_log_encoding) |
|
| 98 |
self.user = repository.find_committer_user(self.committer) |
|
| 99 |
end |
|
| 100 |
|
|
| 101 |
def scan_for_issues |
|
| 102 |
scan_comment_for_issue_ids |
|
| 103 |
end |
|
| 104 |
|
|
| 105 |
TIMELOG_RE = / |
|
| 106 |
( |
|
| 107 |
((\d+)(h|hours?))((\d+)(m|min)?)? |
|
| 108 |
| |
|
| 109 |
((\d+)(h|hours?|m|min)) |
|
| 110 |
| |
|
| 111 |
(\d+):(\d+) |
|
| 112 |
| |
|
| 113 |
(\d+([\.,]\d+)?)h? |
|
| 114 |
) |
|
| 115 |
/x |
|
| 116 |
|
|
| 117 |
def scan_comment_for_issue_ids |
|
| 118 |
return if comments.blank? |
|
| 119 |
# keywords used to reference issues |
|
| 120 |
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
|
| 121 |
ref_keywords_any = ref_keywords.delete('*')
|
|
| 122 |
# keywords used to fix issues |
|
| 123 |
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
|
| 124 |
|
|
| 125 |
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
|
| 126 |
|
|
| 127 |
referenced_issues = [] |
|
| 128 |
|
|
| 129 |
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
|
|
| 130 |
action, refs = match[2], match[3] |
|
| 131 |
next unless action.present? || ref_keywords_any |
|
| 132 |
|
|
| 133 |
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
|
|
| 134 |
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2] |
|
| 135 |
if issue |
|
| 136 |
referenced_issues << issue |
|
| 137 |
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase) |
|
| 138 |
log_time(issue, hours) if hours && Setting.commit_logtime_enabled? |
|
| 139 |
end |
|
| 140 |
end |
|
| 141 |
end |
|
| 142 |
|
|
| 143 |
referenced_issues.uniq! |
|
| 144 |
self.issues = referenced_issues unless referenced_issues.empty? |
|
| 145 |
end |
|
| 146 |
|
|
| 147 |
def short_comments |
|
| 148 |
@short_comments || split_comments.first |
|
| 149 |
end |
|
| 150 |
|
|
| 151 |
def long_comments |
|
| 152 |
@long_comments || split_comments.last |
|
| 153 |
end |
|
| 154 |
|
|
| 155 |
def text_tag(ref_project=nil) |
|
| 156 |
tag = if scmid? |
|
| 157 |
"commit:#{scmid}"
|
|
| 158 |
else |
|
| 159 |
"r#{revision}"
|
|
| 160 |
end |
|
| 161 |
if repository && repository.identifier.present? |
|
| 162 |
tag = "#{repository.identifier}|#{tag}"
|
|
| 163 |
end |
|
| 164 |
if ref_project && project && ref_project != project |
|
| 165 |
tag = "#{project.identifier}:#{tag}"
|
|
| 166 |
end |
|
| 167 |
tag |
|
| 168 |
end |
|
| 169 |
|
|
| 170 |
# Returns the title used for the changeset in the activity/search results |
|
| 171 |
def title |
|
| 172 |
repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
|
|
| 173 |
comm = short_comments.blank? ? '' : (': ' + short_comments)
|
|
| 174 |
"#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
|
|
| 175 |
end |
|
| 176 |
|
|
| 177 |
# Returns the previous changeset |
|
| 178 |
def previous |
|
| 179 |
@previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
|
|
| 180 |
end |
|
| 181 |
|
|
| 182 |
# Returns the next changeset |
|
| 183 |
def next |
|
| 184 |
@next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
|
|
| 185 |
end |
|
| 186 |
|
|
| 187 |
# Creates a new Change from it's common parameters |
|
| 188 |
def create_change(change) |
|
| 189 |
Change.create(:changeset => self, |
|
| 190 |
:action => change[:action], |
|
| 191 |
:path => change[:path], |
|
| 192 |
:from_path => change[:from_path], |
|
| 193 |
:from_revision => change[:from_revision]) |
|
| 194 |
end |
|
| 195 |
|
|
| 196 |
# Finds an issue that can be referenced by the commit message |
|
| 197 |
def find_referenced_issue_by_id(id) |
|
| 198 |
return nil if id.blank? |
|
| 199 |
issue = Issue.find_by_id(id.to_i, :include => :project) |
|
| 200 |
if Setting.commit_cross_project_ref? |
|
| 201 |
# all issues can be referenced/fixed |
|
| 202 |
elsif issue |
|
| 203 |
# issue that belong to the repository project, a subproject or a parent project only |
|
| 204 |
unless issue.project && |
|
| 205 |
(project == issue.project || project.is_ancestor_of?(issue.project) || |
|
| 206 |
project.is_descendant_of?(issue.project)) |
|
| 207 |
issue = nil |
|
| 208 |
end |
|
| 209 |
end |
|
| 210 |
issue |
|
| 211 |
end |
|
| 212 |
|
|
| 213 |
private |
|
| 214 |
|
|
| 215 |
def fix_issue(issue) |
|
| 216 |
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) |
|
| 217 |
if status.nil? |
|
| 218 |
logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
|
|
| 219 |
return issue |
|
| 220 |
end |
|
| 221 |
|
|
| 222 |
# the issue may have been updated by the closure of another one (eg. duplicate) |
|
| 223 |
issue.reload |
|
| 224 |
# don't change the status is the issue is closed |
|
| 225 |
return if issue.status && issue.status.is_closed? |
|
| 226 |
|
|
| 227 |
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project))) |
|
| 228 |
issue.status = status |
|
| 229 |
unless Setting.commit_fix_done_ratio.blank? |
|
| 230 |
issue.done_ratio = Setting.commit_fix_done_ratio.to_i |
|
| 231 |
end |
|
| 232 |
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, |
|
| 233 |
{ :changeset => self, :issue => issue })
|
|
| 234 |
unless issue.save |
|
| 235 |
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
|
|
| 236 |
end |
|
| 237 |
issue |
|
| 238 |
end |
|
| 239 |
|
|
| 240 |
def log_time(issue, hours) |
|
| 241 |
time_entry = TimeEntry.new( |
|
| 242 |
:user => user, |
|
| 243 |
:hours => hours, |
|
| 244 |
:issue => issue, |
|
| 245 |
:spent_on => commit_date, |
|
| 246 |
:comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project), |
|
| 247 |
:locale => Setting.default_language) |
|
| 248 |
) |
|
| 249 |
time_entry.activity = log_time_activity unless log_time_activity.nil? |
|
| 250 |
|
|
| 251 |
unless time_entry.save |
|
| 252 |
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
|
|
| 253 |
end |
|
| 254 |
time_entry |
|
| 255 |
end |
|
| 256 |
|
|
| 257 |
def log_time_activity |
|
| 258 |
if Setting.commit_logtime_activity_id.to_i > 0 |
|
| 259 |
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) |
|
| 260 |
end |
|
| 261 |
end |
|
| 262 |
|
|
| 263 |
def split_comments |
|
| 264 |
comments =~ /\A(.+?)\r?\n(.*)$/m |
|
| 265 |
@short_comments = $1 || comments |
|
| 266 |
@long_comments = $2.to_s.strip |
|
| 267 |
return @short_comments, @long_comments |
|
| 268 |
end |
|
| 269 |
|
|
| 270 |
public |
|
| 271 |
|
|
| 272 |
# Strips and reencodes a commit log before insertion into the database |
|
| 273 |
def self.normalize_comments(str, encoding) |
|
| 274 |
Changeset.to_utf8(str.to_s.strip, encoding) |
|
| 275 |
end |
|
| 276 |
|
|
| 277 |
def self.to_utf8(str, encoding) |
|
| 278 |
Redmine::CodesetUtil.to_utf8(str, encoding) |
|
| 279 |
end |
|
| 280 |
end |
|
| .svn/pristine/20/2069c7a6894bd57d0791bf6d175776952425d0ca.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
require 'diff' |
|
| 19 |
|
|
| 20 |
# The WikiController follows the Rails REST controller pattern but with |
|
| 21 |
# a few differences |
|
| 22 |
# |
|
| 23 |
# * index - shows a list of WikiPages grouped by page or date |
|
| 24 |
# * new - not used |
|
| 25 |
# * create - not used |
|
| 26 |
# * show - will also show the form for creating a new wiki page |
|
| 27 |
# * edit - used to edit an existing or new page |
|
| 28 |
# * update - used to save a wiki page update to the database, including new pages |
|
| 29 |
# * destroy - normal |
|
| 30 |
# |
|
| 31 |
# Other member and collection methods are also used |
|
| 32 |
# |
|
| 33 |
# TODO: still being worked on |
|
| 34 |
class WikiController < ApplicationController |
|
| 35 |
default_search_scope :wiki_pages |
|
| 36 |
before_filter :find_wiki, :authorize |
|
| 37 |
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update] |
|
| 38 |
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] |
|
| 39 |
accept_api_auth :index, :show, :update, :destroy |
|
| 40 |
|
|
| 41 |
helper :attachments |
|
| 42 |
include AttachmentsHelper |
|
| 43 |
helper :watchers |
|
| 44 |
include Redmine::Export::PDF |
|
| 45 |
|
|
| 46 |
# List of pages, sorted alphabetically and by parent (hierarchy) |
|
| 47 |
def index |
|
| 48 |
load_pages_for_index |
|
| 49 |
|
|
| 50 |
respond_to do |format| |
|
| 51 |
format.html {
|
|
| 52 |
@pages_by_parent_id = @pages.group_by(&:parent_id) |
|
| 53 |
} |
|
| 54 |
format.api |
|
| 55 |
end |
|
| 56 |
end |
|
| 57 |
|
|
| 58 |
# List of page, by last update |
|
| 59 |
def date_index |
|
| 60 |
load_pages_for_index |
|
| 61 |
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
|
|
| 62 |
end |
|
| 63 |
|
|
| 64 |
# display a page (in editing mode if it doesn't exist) |
|
| 65 |
def show |
|
| 66 |
if @page.new_record? |
|
| 67 |
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request? |
|
| 68 |
edit |
|
| 69 |
render :action => 'edit' |
|
| 70 |
else |
|
| 71 |
render_404 |
|
| 72 |
end |
|
| 73 |
return |
|
| 74 |
end |
|
| 75 |
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project) |
|
| 76 |
deny_access |
|
| 77 |
return |
|
| 78 |
end |
|
| 79 |
@content = @page.content_for_version(params[:version]) |
|
| 80 |
if User.current.allowed_to?(:export_wiki_pages, @project) |
|
| 81 |
if params[:format] == 'pdf' |
|
| 82 |
send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
|
|
| 83 |
return |
|
| 84 |
elsif params[:format] == 'html' |
|
| 85 |
export = render_to_string :action => 'export', :layout => false |
|
| 86 |
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
|
|
| 87 |
return |
|
| 88 |
elsif params[:format] == 'txt' |
|
| 89 |
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
|
|
| 90 |
return |
|
| 91 |
end |
|
| 92 |
end |
|
| 93 |
@editable = editable? |
|
| 94 |
@sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) && |
|
| 95 |
@content.current_version? && |
|
| 96 |
Redmine::WikiFormatting.supports_section_edit? |
|
| 97 |
|
|
| 98 |
respond_to do |format| |
|
| 99 |
format.html |
|
| 100 |
format.api |
|
| 101 |
end |
|
| 102 |
end |
|
| 103 |
|
|
| 104 |
# edit an existing page or a new one |
|
| 105 |
def edit |
|
| 106 |
return render_403 unless editable? |
|
| 107 |
if @page.new_record? |
|
| 108 |
@page.content = WikiContent.new(:page => @page) |
|
| 109 |
if params[:parent].present? |
|
| 110 |
@page.parent = @page.wiki.find_page(params[:parent].to_s) |
|
| 111 |
end |
|
| 112 |
end |
|
| 113 |
|
|
| 114 |
@content = @page.content_for_version(params[:version]) |
|
| 115 |
@content.text = initial_page_content(@page) if @content.text.blank? |
|
| 116 |
# don't keep previous comment |
|
| 117 |
@content.comments = nil |
|
| 118 |
|
|
| 119 |
# To prevent StaleObjectError exception when reverting to a previous version |
|
| 120 |
@content.version = @page.content.version |
|
| 121 |
|
|
| 122 |
@text = @content.text |
|
| 123 |
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? |
|
| 124 |
@section = params[:section].to_i |
|
| 125 |
@text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section) |
|
| 126 |
render_404 if @text.blank? |
|
| 127 |
end |
|
| 128 |
end |
|
| 129 |
|
|
| 130 |
# Creates a new page or updates an existing one |
|
| 131 |
def update |
|
| 132 |
return render_403 unless editable? |
|
| 133 |
was_new_page = @page.new_record? |
|
| 134 |
@page.content = WikiContent.new(:page => @page) if @page.new_record? |
|
| 135 |
@page.safe_attributes = params[:wiki_page] |
|
| 136 |
|
|
| 137 |
@content = @page.content |
|
| 138 |
content_params = params[:content] |
|
| 139 |
if content_params.nil? && params[:wiki_page].is_a?(Hash) |
|
| 140 |
content_params = params[:wiki_page].slice(:text, :comments, :version) |
|
| 141 |
end |
|
| 142 |
content_params ||= {}
|
|
| 143 |
|
|
| 144 |
@content.comments = content_params[:comments] |
|
| 145 |
@text = content_params[:text] |
|
| 146 |
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? |
|
| 147 |
@section = params[:section].to_i |
|
| 148 |
@section_hash = params[:section_hash] |
|
| 149 |
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash) |
|
| 150 |
else |
|
| 151 |
@content.version = content_params[:version] if content_params[:version] |
|
| 152 |
@content.text = @text |
|
| 153 |
end |
|
| 154 |
@content.author = User.current |
|
| 155 |
|
|
| 156 |
if @page.save_with_content |
|
| 157 |
attachments = Attachment.attach_files(@page, params[:attachments]) |
|
| 158 |
render_attachment_warning_if_needed(@page) |
|
| 159 |
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
|
|
| 160 |
|
|
| 161 |
respond_to do |format| |
|
| 162 |
format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
|
|
| 163 |
format.api {
|
|
| 164 |
if was_new_page |
|
| 165 |
render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title) |
|
| 166 |
else |
|
| 167 |
render_api_ok |
|
| 168 |
end |
|
| 169 |
} |
|
| 170 |
end |
|
| 171 |
else |
|
| 172 |
respond_to do |format| |
|
| 173 |
format.html { render :action => 'edit' }
|
|
| 174 |
format.api { render_validation_errors(@content) }
|
|
| 175 |
end |
|
| 176 |
end |
|
| 177 |
|
|
| 178 |
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError |
|
| 179 |
# Optimistic locking exception |
|
| 180 |
respond_to do |format| |
|
| 181 |
format.html {
|
|
| 182 |
flash.now[:error] = l(:notice_locking_conflict) |
|
| 183 |
render :action => 'edit' |
|
| 184 |
} |
|
| 185 |
format.api { render_api_head :conflict }
|
|
| 186 |
end |
|
| 187 |
rescue ActiveRecord::RecordNotSaved |
|
| 188 |
respond_to do |format| |
|
| 189 |
format.html { render :action => 'edit' }
|
|
| 190 |
format.api { render_validation_errors(@content) }
|
|
| 191 |
end |
|
| 192 |
end |
|
| 193 |
|
|
| 194 |
# rename a page |
|
| 195 |
def rename |
|
| 196 |
return render_403 unless editable? |
|
| 197 |
@page.redirect_existing_links = true |
|
| 198 |
# used to display the *original* title if some AR validation errors occur |
|
| 199 |
@original_title = @page.pretty_title |
|
| 200 |
if request.post? && @page.update_attributes(params[:wiki_page]) |
|
| 201 |
flash[:notice] = l(:notice_successful_update) |
|
| 202 |
redirect_to :action => 'show', :project_id => @project, :id => @page.title |
|
| 203 |
end |
|
| 204 |
end |
|
| 205 |
|
|
| 206 |
def protect |
|
| 207 |
@page.update_attribute :protected, params[:protected] |
|
| 208 |
redirect_to :action => 'show', :project_id => @project, :id => @page.title |
|
| 209 |
end |
|
| 210 |
|
|
| 211 |
# show page history |
|
| 212 |
def history |
|
| 213 |
@version_count = @page.content.versions.count |
|
| 214 |
@version_pages = Paginator.new self, @version_count, per_page_option, params['page'] |
|
| 215 |
# don't load text |
|
| 216 |
@versions = @page.content.versions.find :all, |
|
| 217 |
:select => "id, author_id, comments, updated_on, version", |
|
| 218 |
:order => 'version DESC', |
|
| 219 |
:limit => @version_pages.items_per_page + 1, |
|
| 220 |
:offset => @version_pages.current.offset |
|
| 221 |
|
|
| 222 |
render :layout => false if request.xhr? |
|
| 223 |
end |
|
| 224 |
|
|
| 225 |
def diff |
|
| 226 |
@diff = @page.diff(params[:version], params[:version_from]) |
|
| 227 |
render_404 unless @diff |
|
| 228 |
end |
|
| 229 |
|
|
| 230 |
def annotate |
|
| 231 |
@annotate = @page.annotate(params[:version]) |
|
| 232 |
render_404 unless @annotate |
|
| 233 |
end |
|
| 234 |
|
|
| 235 |
# Removes a wiki page and its history |
|
| 236 |
# Children can be either set as root pages, removed or reassigned to another parent page |
|
| 237 |
def destroy |
|
| 238 |
return render_403 unless editable? |
|
| 239 |
|
|
| 240 |
@descendants_count = @page.descendants.size |
|
| 241 |
if @descendants_count > 0 |
|
| 242 |
case params[:todo] |
|
| 243 |
when 'nullify' |
|
| 244 |
# Nothing to do |
|
| 245 |
when 'destroy' |
|
| 246 |
# Removes all its descendants |
|
| 247 |
@page.descendants.each(&:destroy) |
|
| 248 |
when 'reassign' |
|
| 249 |
# Reassign children to another parent page |
|
| 250 |
reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i) |
|
| 251 |
return unless reassign_to |
|
| 252 |
@page.children.each do |child| |
|
| 253 |
child.update_attribute(:parent, reassign_to) |
|
| 254 |
end |
|
| 255 |
else |
|
| 256 |
@reassignable_to = @wiki.pages - @page.self_and_descendants |
|
| 257 |
# display the destroy form if it's a user request |
|
| 258 |
return unless api_request? |
|
| 259 |
end |
|
| 260 |
end |
|
| 261 |
@page.destroy |
|
| 262 |
respond_to do |format| |
|
| 263 |
format.html { redirect_to :action => 'index', :project_id => @project }
|
|
| 264 |
format.api { render_api_ok }
|
|
| 265 |
end |
|
| 266 |
end |
|
| 267 |
|
|
| 268 |
def destroy_version |
|
| 269 |
return render_403 unless editable? |
|
| 270 |
|
|
| 271 |
@content = @page.content_for_version(params[:version]) |
|
| 272 |
@content.destroy |
|
| 273 |
redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project |
|
| 274 |
end |
|
| 275 |
|
|
| 276 |
# Export wiki to a single pdf or html file |
|
| 277 |
def export |
|
| 278 |
@pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
|
|
| 279 |
respond_to do |format| |
|
| 280 |
format.html {
|
|
| 281 |
export = render_to_string :action => 'export_multiple', :layout => false |
|
| 282 |
send_data(export, :type => 'text/html', :filename => "wiki.html") |
|
| 283 |
} |
|
| 284 |
format.pdf {
|
|
| 285 |
send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
|
|
| 286 |
} |
|
| 287 |
end |
|
| 288 |
end |
|
| 289 |
|
|
| 290 |
def preview |
|
| 291 |
page = @wiki.find_page(params[:id]) |
|
| 292 |
# page is nil when previewing a new page |
|
| 293 |
return render_403 unless page.nil? || editable?(page) |
|
| 294 |
if page |
|
| 295 |
@attachements = page.attachments |
|
| 296 |
@previewed = page.content |
|
| 297 |
end |
|
| 298 |
@text = params[:content][:text] |
|
| 299 |
render :partial => 'common/preview' |
|
| 300 |
end |
|
| 301 |
|
|
| 302 |
def add_attachment |
|
| 303 |
return render_403 unless editable? |
|
| 304 |
attachments = Attachment.attach_files(@page, params[:attachments]) |
|
| 305 |
render_attachment_warning_if_needed(@page) |
|
| 306 |
redirect_to :action => 'show', :id => @page.title, :project_id => @project |
|
| 307 |
end |
|
| 308 |
|
|
| 309 |
private |
|
| 310 |
|
|
| 311 |
def find_wiki |
|
| 312 |
@project = Project.find(params[:project_id]) |
|
| 313 |
@wiki = @project.wiki |
|
| 314 |
render_404 unless @wiki |
|
| 315 |
rescue ActiveRecord::RecordNotFound |
|
| 316 |
render_404 |
|
| 317 |
end |
|
| 318 |
|
|
| 319 |
# Finds the requested page or a new page if it doesn't exist |
|
| 320 |
def find_existing_or_new_page |
|
| 321 |
@page = @wiki.find_or_new_page(params[:id]) |
|
| 322 |
if @wiki.page_found_with_redirect? |
|
| 323 |
redirect_to params.update(:id => @page.title) |
|
| 324 |
end |
|
| 325 |
end |
|
| 326 |
|
|
| 327 |
# Finds the requested page and returns a 404 error if it doesn't exist |
|
| 328 |
def find_existing_page |
|
| 329 |
@page = @wiki.find_page(params[:id]) |
|
| 330 |
if @page.nil? |
|
| 331 |
render_404 |
|
| 332 |
return |
|
| 333 |
end |
|
| 334 |
if @wiki.page_found_with_redirect? |
|
| 335 |
redirect_to params.update(:id => @page.title) |
|
| 336 |
end |
|
| 337 |
end |
|
| 338 |
|
|
| 339 |
# Returns true if the current user is allowed to edit the page, otherwise false |
|
| 340 |
def editable?(page = @page) |
|
| 341 |
page.editable_by?(User.current) |
|
| 342 |
end |
|
| 343 |
|
|
| 344 |
# Returns the default content of a new wiki page |
|
| 345 |
def initial_page_content(page) |
|
| 346 |
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
| 347 |
extend helper unless self.instance_of?(helper) |
|
| 348 |
helper.instance_method(:initial_page_content).bind(self).call(page) |
|
| 349 |
end |
|
| 350 |
|
|
| 351 |
def load_pages_for_index |
|
| 352 |
@pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
|
|
| 353 |
end |
|
| 354 |
end |
|
| .svn/pristine/20/209f9ebc3aa58812be26dbeb5ba5ab0a2d6a75b3.svn-base | ||
|---|---|---|
| 1 |
# encoding: utf-8 |
|
| 2 |
# |
|
| 3 |
# Helpers to sort tables using clickable column headers. |
|
| 4 |
# |
|
| 5 |
# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005. |
|
| 6 |
# Jean-Philippe Lang, 2009 |
|
| 7 |
# License: This source code is released under the MIT license. |
|
| 8 |
# |
|
| 9 |
# - Consecutive clicks toggle the column's sort order. |
|
| 10 |
# - Sort state is maintained by a session hash entry. |
|
| 11 |
# - CSS classes identify sort column and state. |
|
| 12 |
# - Typically used in conjunction with the Pagination module. |
|
| 13 |
# |
|
| 14 |
# Example code snippets: |
|
| 15 |
# |
|
| 16 |
# Controller: |
|
| 17 |
# |
|
| 18 |
# helper :sort |
|
| 19 |
# include SortHelper |
|
| 20 |
# |
|
| 21 |
# def list |
|
| 22 |
# sort_init 'last_name' |
|
| 23 |
# sort_update %w(first_name last_name) |
|
| 24 |
# @items = Contact.find_all nil, sort_clause |
|
| 25 |
# end |
|
| 26 |
# |
|
| 27 |
# Controller (using Pagination module): |
|
| 28 |
# |
|
| 29 |
# helper :sort |
|
| 30 |
# include SortHelper |
|
| 31 |
# |
|
| 32 |
# def list |
|
| 33 |
# sort_init 'last_name' |
|
| 34 |
# sort_update %w(first_name last_name) |
|
| 35 |
# @contact_pages, @items = paginate :contacts, |
|
| 36 |
# :order_by => sort_clause, |
|
| 37 |
# :per_page => 10 |
|
| 38 |
# end |
|
| 39 |
# |
|
| 40 |
# View (table header in list.rhtml): |
|
| 41 |
# |
|
| 42 |
# <thead> |
|
| 43 |
# <tr> |
|
| 44 |
# <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
|
|
| 45 |
# <%= sort_header_tag('last_name', :caption => 'Name') %>
|
|
| 46 |
# <%= sort_header_tag('phone') %>
|
|
| 47 |
# <%= sort_header_tag('address', :width => 200) %>
|
|
| 48 |
# </tr> |
|
| 49 |
# </thead> |
|
| 50 |
# |
|
| 51 |
# - Introduces instance variables: @sort_default, @sort_criteria |
|
| 52 |
# - Introduces param :sort |
|
| 53 |
# |
|
| 54 |
|
|
| 55 |
module SortHelper |
|
| 56 |
class SortCriteria |
|
| 57 |
|
|
| 58 |
def initialize |
|
| 59 |
@criteria = [] |
|
| 60 |
end |
|
| 61 |
|
|
| 62 |
def available_criteria=(criteria) |
|
| 63 |
unless criteria.is_a?(Hash) |
|
| 64 |
criteria = criteria.inject({}) {|h,k| h[k] = k; h}
|
|
| 65 |
end |
|
| 66 |
@available_criteria = criteria |
|
| 67 |
end |
|
| 68 |
|
|
| 69 |
def from_param(param) |
|
| 70 |
@criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
|
|
| 71 |
normalize! |
|
| 72 |
end |
|
| 73 |
|
|
| 74 |
def criteria=(arg) |
|
| 75 |
@criteria = arg |
|
| 76 |
normalize! |
|
| 77 |
end |
|
| 78 |
|
|
| 79 |
def to_param |
|
| 80 |
@criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
|
|
| 81 |
end |
|
| 82 |
|
|
| 83 |
def to_sql |
|
| 84 |
sql = @criteria.collect do |k,o| |
|
| 85 |
if s = @available_criteria[k] |
|
| 86 |
(o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ')
|
|
| 87 |
end |
|
| 88 |
end.compact.join(', ')
|
|
| 89 |
sql.blank? ? nil : sql |
|
| 90 |
end |
|
| 91 |
|
|
| 92 |
def to_a |
|
| 93 |
@criteria.dup |
|
| 94 |
end |
|
| 95 |
|
|
| 96 |
def add!(key, asc) |
|
| 97 |
@criteria.delete_if {|k,o| k == key}
|
|
| 98 |
@criteria = [[key, asc]] + @criteria |
|
| 99 |
normalize! |
|
| 100 |
end |
|
| 101 |
|
|
| 102 |
def add(*args) |
|
| 103 |
r = self.class.new.from_param(to_param) |
|
| 104 |
r.add!(*args) |
|
| 105 |
r |
|
| 106 |
end |
|
| 107 |
|
|
| 108 |
def first_key |
|
| 109 |
@criteria.first && @criteria.first.first |
|
| 110 |
end |
|
| 111 |
|
|
| 112 |
def first_asc? |
|
| 113 |
@criteria.first && @criteria.first.last |
|
| 114 |
end |
|
| 115 |
|
|
| 116 |
def empty? |
|
| 117 |
@criteria.empty? |
|
| 118 |
end |
|
| 119 |
|
|
| 120 |
private |
|
| 121 |
|
|
| 122 |
def normalize! |
|
| 123 |
@criteria ||= [] |
|
| 124 |
@criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
|
|
| 125 |
@criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
|
|
| 126 |
@criteria.slice!(3) |
|
| 127 |
self |
|
| 128 |
end |
|
| 129 |
|
|
| 130 |
# Appends DESC to the sort criterion unless it has a fixed order |
|
| 131 |
def append_desc(criterion) |
|
| 132 |
if criterion =~ / (asc|desc)$/i |
|
| 133 |
criterion |
|
| 134 |
else |
|
| 135 |
"#{criterion} DESC"
|
|
| 136 |
end |
|
| 137 |
end |
|
| 138 |
end |
|
| 139 |
|
|
| 140 |
def sort_name |
|
| 141 |
controller_name + '_' + action_name + '_sort' |
|
| 142 |
end |
|
| 143 |
|
|
| 144 |
# Initializes the default sort. |
|
| 145 |
# Examples: |
|
| 146 |
# |
|
| 147 |
# sort_init 'name' |
|
| 148 |
# sort_init 'id', 'desc' |
|
| 149 |
# sort_init ['name', ['id', 'desc']] |
|
| 150 |
# sort_init [['name', 'desc'], ['id', 'desc']] |
|
| 151 |
# |
|
| 152 |
def sort_init(*args) |
|
| 153 |
case args.size |
|
| 154 |
when 1 |
|
| 155 |
@sort_default = args.first.is_a?(Array) ? args.first : [[args.first]] |
|
| 156 |
when 2 |
|
| 157 |
@sort_default = [[args.first, args.last]] |
|
| 158 |
else |
|
| 159 |
raise ArgumentError |
|
| 160 |
end |
|
| 161 |
end |
|
| 162 |
|
|
| 163 |
# Updates the sort state. Call this in the controller prior to calling |
|
| 164 |
# sort_clause. |
|
| 165 |
# - criteria can be either an array or a hash of allowed keys |
|
| 166 |
# |
|
| 167 |
def sort_update(criteria, sort_name=nil) |
|
| 168 |
sort_name ||= self.sort_name |
|
| 169 |
@sort_criteria = SortCriteria.new |
|
| 170 |
@sort_criteria.available_criteria = criteria |
|
| 171 |
@sort_criteria.from_param(params[:sort] || session[sort_name]) |
|
| 172 |
@sort_criteria.criteria = @sort_default if @sort_criteria.empty? |
|
| 173 |
session[sort_name] = @sort_criteria.to_param |
|
| 174 |
end |
|
| 175 |
|
|
| 176 |
# Clears the sort criteria session data |
|
| 177 |
# |
|
| 178 |
def sort_clear |
|
| 179 |
session[sort_name] = nil |
|
| 180 |
end |
|
| 181 |
|
|
| 182 |
# Returns an SQL sort clause corresponding to the current sort state. |
|
| 183 |
# Use this to sort the controller's table items collection. |
|
| 184 |
# |
|
| 185 |
def sort_clause() |
|
| 186 |
@sort_criteria.to_sql |
|
| 187 |
end |
|
| 188 |
|
|
| 189 |
def sort_criteria |
|
| 190 |
@sort_criteria |
|
| 191 |
end |
|
| 192 |
|
|
| 193 |
# Returns a link which sorts by the named column. |
|
| 194 |
# |
|
| 195 |
# - column is the name of an attribute in the sorted record collection. |
|
| 196 |
# - the optional caption explicitly specifies the displayed link text. |
|
| 197 |
# - 2 CSS classes reflect the state of the link: sort and asc or desc |
|
| 198 |
# |
|
| 199 |
def sort_link(column, caption, default_order) |
|
| 200 |
css, order = nil, default_order |
|
| 201 |
|
|
| 202 |
if column.to_s == @sort_criteria.first_key |
|
| 203 |
if @sort_criteria.first_asc? |
|
| 204 |
css = 'sort asc' |
|
| 205 |
order = 'desc' |
|
| 206 |
else |
|
| 207 |
css = 'sort desc' |
|
| 208 |
order = 'asc' |
|
| 209 |
end |
|
| 210 |
end |
|
| 211 |
caption = column.to_s.humanize unless caption |
|
| 212 |
|
|
| 213 |
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
|
|
| 214 |
url_options = params.merge(sort_options) |
|
| 215 |
|
|
| 216 |
# Add project_id to url_options |
|
| 217 |
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) |
|
| 218 |
|
|
| 219 |
link_to_content_update(h(caption), url_options, :class => css) |
|
| 220 |
end |
|
| 221 |
|
|
| 222 |
# Returns a table header <th> tag with a sort link for the named column |
|
| 223 |
# attribute. |
|
| 224 |
# |
|
| 225 |
# Options: |
|
| 226 |
# :caption The displayed link name (defaults to titleized column name). |
|
| 227 |
# :title The tag's 'title' attribute (defaults to 'Sort by :caption'). |
|
| 228 |
# |
|
| 229 |
# Other options hash entries generate additional table header tag attributes. |
|
| 230 |
# |
|
| 231 |
# Example: |
|
| 232 |
# |
|
| 233 |
# <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
|
|
| 234 |
# |
|
| 235 |
def sort_header_tag(column, options = {})
|
|
| 236 |
caption = options.delete(:caption) || column.to_s.humanize |
|
| 237 |
default_order = options.delete(:default_order) || 'asc' |
|
| 238 |
options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
|
|
| 239 |
content_tag('th', sort_link(column, caption, default_order), options)
|
|
| 240 |
end |
|
| 241 |
end |
|
| 242 |
|
|
| .svn/pristine/20/20b8ba7bb15b0efb2e3e09bae49971ba5e4c3a84.svn-base | ||
|---|---|---|
| 1 |
<h2><%=l(:label_password_lost)%></h2> |
|
| 2 |
|
|
| 3 |
<%= error_messages_for 'user' %> |
|
| 4 |
|
|
| 5 |
<%= form_tag(lost_password_path) do %> |
|
| 6 |
<%= hidden_field_tag 'token', @token.value %> |
|
| 7 |
<div class="box tabular"> |
|
| 8 |
<p> |
|
| 9 |
<label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label> |
|
| 10 |
<%= password_field_tag 'new_password', nil, :size => 25 %> |
|
| 11 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em> |
|
| 12 |
</p> |
|
| 13 |
|
|
| 14 |
<p> |
|
| 15 |
<label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label> |
|
| 16 |
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %> |
|
| 17 |
</p> |
|
| 18 |
</div> |
|
| 19 |
<p><%= submit_tag l(:button_save) %></p> |
|
| 20 |
<% end %> |
|
| .svn/pristine/20/20e5c0c48e6b23cc5129695034ba2f44417b7624.svn-base | ||
|---|---|---|
| 1 |
<h2><%= l(:label_spent_time) %></h2> |
|
| 2 |
|
|
| 3 |
<%= labelled_form_for @time_entry, :url => time_entries_path do |f| %> |
|
| 4 |
<%= hidden_field_tag 'project_id', params[:project_id] if params[:project_id] %> |
|
| 5 |
<%= render :partial => 'form', :locals => {:f => f} %>
|
|
| 6 |
<%= submit_tag l(:button_create) %> |
|
| 7 |
<%= submit_tag l(:button_create_and_continue), :name => 'continue' %> |
|
| 8 |
<% end %> |
|
| .svn/pristine/20/20f8dff24ed27f7604eb718009f2a8bf899ab23c.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
require File.expand_path('../../../test_helper', __FILE__)
|
|
| 19 |
require 'pp' |
|
| 20 |
class ApiTest::NewsTest < ActionController::IntegrationTest |
|
| 21 |
fixtures :projects, :trackers, :issue_statuses, :issues, |
|
| 22 |
:enumerations, :users, :issue_categories, |
|
| 23 |
:projects_trackers, |
|
| 24 |
:roles, |
|
| 25 |
:member_roles, |
|
| 26 |
:members, |
|
| 27 |
:enabled_modules, |
|
| 28 |
:workflows, |
|
| 29 |
:news |
|
| 30 |
|
|
| 31 |
def setup |
|
| 32 |
Setting.rest_api_enabled = '1' |
|
| 33 |
end |
|
| 34 |
|
|
| 35 |
context "GET /news" do |
|
| 36 |
context ".xml" do |
|
| 37 |
should "return news" do |
|
| 38 |
get '/news.xml' |
|
| 39 |
|
|
| 40 |
assert_tag :tag => 'news', |
|
| 41 |
:attributes => {:type => 'array'},
|
|
| 42 |
:child => {
|
|
| 43 |
:tag => 'news', |
|
| 44 |
:child => {
|
|
| 45 |
:tag => 'id', |
|
| 46 |
:content => '2' |
|
| 47 |
} |
|
| 48 |
} |
|
| 49 |
end |
|
| 50 |
end |
|
| 51 |
|
|
| 52 |
context ".json" do |
|
| 53 |
should "return news" do |
|
| 54 |
get '/news.json' |
|
| 55 |
|
|
| 56 |
json = ActiveSupport::JSON.decode(response.body) |
|
| 57 |
assert_kind_of Hash, json |
|
| 58 |
assert_kind_of Array, json['news'] |
|
| 59 |
assert_kind_of Hash, json['news'].first |
|
| 60 |
assert_equal 2, json['news'].first['id'] |
|
| 61 |
end |
|
| 62 |
end |
|
| 63 |
end |
|
| 64 |
|
|
| 65 |
context "GET /projects/:project_id/news" do |
|
| 66 |
context ".xml" do |
|
| 67 |
should_allow_api_authentication(:get, "/projects/onlinestore/news.xml") |
|
| 68 |
|
|
| 69 |
should "return news" do |
|
| 70 |
get '/projects/ecookbook/news.xml' |
|
| 71 |
|
|
| 72 |
assert_tag :tag => 'news', |
|
| 73 |
:attributes => {:type => 'array'},
|
|
| 74 |
:child => {
|
|
| 75 |
:tag => 'news', |
|
| 76 |
:child => {
|
|
| 77 |
:tag => 'id', |
|
| 78 |
:content => '2' |
|
| 79 |
} |
|
| 80 |
} |
|
| 81 |
end |
|
| 82 |
end |
|
| 83 |
|
|
| 84 |
context ".json" do |
|
| 85 |
should_allow_api_authentication(:get, "/projects/onlinestore/news.json") |
|
| 86 |
|
|
| 87 |
should "return news" do |
|
| 88 |
get '/projects/ecookbook/news.json' |
|
| 89 |
|
|
| 90 |
json = ActiveSupport::JSON.decode(response.body) |
|
| 91 |
assert_kind_of Hash, json |
|
| 92 |
assert_kind_of Array, json['news'] |
|
| 93 |
assert_kind_of Hash, json['news'].first |
|
| 94 |
assert_equal 2, json['news'].first['id'] |
|
| 95 |
end |
|
| 96 |
end |
|
| 97 |
end |
|
| 98 |
end |
|
Also available in: Unified diff