Revision 1297:0a574315af3e .svn/pristine/20

View differences:

.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